← Recipes

阅读中文版本

Debug Duplicate Webhook Deliveries

Correlate webhook delivery ids and timestamps to distinguish provider retries from duplicate events emitted by your own pipeline.

~30 minwebhookapiidempotency

Task

Use delivery-id deduplication and timestamp correlation to locate the source of repeated webhook processing.

When to use this

Orders are created twice because a webhook appears to fire more than once, but it is unclear whether the provider retried or the consumer reprocessed.

Prerequisites

  • Webhook events stored with raw headers and bodies
  • A unique delivery id in the provider payload
  • Query access to the consumer database

Steps

1. Deduplicate by provider delivery id before any business logic runs.

const seen = new Set();
for (const event of events) {
  if (seen.has(event.deliveryId)) continue;
  seen.add(event.deliveryId);
  await process(event);
}

Expected output: Each provider delivery id is processed at most once.

2. Correlate repeated deliveries by timestamp to tell retries from re-emits.

SELECT delivery_id, count(*) AS attempts, max(received_at) - min(received_at) AS window
FROM webhook_events
GROUP BY delivery_id
HAVING count(*) > 1
ORDER BY window DESC;

Expected output: Delivery ids repeated within the provider retry window indicate genuine retries rather than new events.

Troubleshooting

Problem

Delivery id is missing on legacy payloads

Solution

Fall back to a hash of body plus event type, and backfill ids once the provider adds them.

Problem

Business logic runs before dedup

Solution

Move the dedup check into the ingestion boundary so side effects never happen twice.

Limitations

  • Requires the provider to expose a stable delivery id.
  • Dedup store must be durable and shared across consumers.
  • Does not address providers that emit genuinely distinct duplicate events.

What this recipe does

Use delivery-id deduplication and timestamp correlation to locate the source of repeated webhook processing.

Orders are created twice because a webhook appears to fire more than once, but it is unclear whether the provider retried or the consumer reprocessed.

Prerequisites

  • Webhook events stored with raw headers and bodies
  • A unique delivery id in the provider payload
  • Query access to the consumer database

Procedure

  1. Deduplicate by provider delivery id before any business logic runs.
const seen = new Set();
for (const event of events) {
  if (seen.has(event.deliveryId)) continue;
  seen.add(event.deliveryId);
  await process(event);
}

Expected result: Each provider delivery id is processed at most once.

  1. Correlate repeated deliveries by timestamp to tell retries from re-emits.
SELECT delivery_id, count(*) AS attempts, max(received_at) - min(received_at) AS window
FROM webhook_events
GROUP BY delivery_id
HAVING count(*) > 1
ORDER BY window DESC;

Expected result: Delivery ids repeated within the provider retry window indicate genuine retries rather than new events.

How to confirm it worked

Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows the count of repeated business rows drops to zero and the correlation query shows only expected provider retries.

Troubleshooting

Delivery id is missing on legacy payloads — Fall back to a hash of body plus event type, and backfill ids once the provider adds them.

Business logic runs before dedup — Move the dedup check into the ingestion boundary so side effects never happen twice.

Limitations

  • Requires the provider to expose a stable delivery id.
  • Dedup store must be durable and shared across consumers.
  • Does not address providers that emit genuinely distinct duplicate events.