48-hour launch: 40% off with code LAUNCH40, Guide $5.40 / Kit $11.40

Stripe webhook duplicate events: making your handler idempotent

Stripe's docs say endpoints might occasionally receive the same event more than once. Every retry after a timeout or non-2xx response is another delivery of the same event. If your handler isn't idempotent, you'll get double emails, double credits or double fulfillment.

1. De-duplicate on event.id, atomically

-- one row per processed event
CREATE TABLE stripe_events (id text PRIMARY KEY, processed_at timestamptz DEFAULT now());

// in the worker
const inserted = await db.query(
  'INSERT INTO stripe_events (id) VALUES ($1) ON CONFLICT DO NOTHING RETURNING id', [event.id]);
if (inserted.rowCount === 0) return;          // already handled
await fulfil(event);

"Check, then insert" has a race when two deliveries arrive together. The unique constraint makes it atomic. If fulfillment can fail after the insert, wrap both in one transaction, or mark the event "processing" and then "processed" so a failure can be retried.

2. Handle the two-Event case

Stripe notes that sometimes two separate Event objects are generated for the same change. They have different event.id values, so an event-ID check won't catch them. Its advice is to identify these by the ID of the object in data.object together with event.type. In practice, make the side effect idempotent too: for example, upsert the order keyed on the PaymentIntent or Checkout Session ID.

Duplicates usually start as retries. The guide covers every common Stripe webhook failure with copy-paste fixes. The kit is a self-hosted Cloudflare Worker relay that verifies Stripe signatures, then retries, replays or dead-letters each event instead of dropping it. It also de-duplicates with idempotency keys.

Fix Your Stripe Webhooks guide · Self-Hosted Webhook Relay Kit. Code LAUNCH40 = 40% off until Sun Oct 11, 6:40 AM MT.

3. Don't depend on order

Stripe doesn't guarantee events arrive in the order they were created, and the created timestamp is in seconds, so two events can share one. Don't use created to detect duplicates or to order events. When order matters, fetch the current object from the API.

4. Return 2xx for duplicates

If a duplicate gets an error response, Stripe keeps retrying it. Answer 200 for events you've already handled.

Duplicates often start as timeouts. See Stripe webhook timed out.

Free download: Webhook debugging cheat sheet.

Still failing? Paste your payload, header and secret into the free Stripe signature checker. It runs in your browser and tells you which cause it is.

All five common causes with copy-paste fixes: Fix Your Stripe Webhooks guide ($9). Retries, replay and a dead-letter list in front of your endpoint: the self-hosted relay kit ($19).