Stripe's Webhook Retry Trap: Why Your Integration Processes the Same Payment Twice

Stripe doesn't guarantee exactly-once webhook delivery. That duplicate order confirmation isn't a bug -- it's expected behavior you haven't designed for yet.

·4 min read

The symptom: a payment event that fires more than once

A merchant's integration listens for payment_intent.succeeded on Stripe, and on receipt it marks the order paid, decrements inventory, and sends a confirmation email. Most of the time this works perfectly. Then, occasionally, a customer gets two confirmation emails for one order, or an inventory count drops by two units for a single sale. The team assumes their code has a bug -- a duplicate loop, a double form submission, a race condition somewhere. Often it's none of those. Stripe delivered the exact same webhook event twice, and the handler had no way of knowing it had already seen it.

Why this happens: Stripe's delivery guarantee is "at least once," not "exactly once"

This is explicitly documented Stripe behavior, not an edge-case bug: Stripe's webhook system guarantees that an event will be delivered, but it does not guarantee it will be delivered only once. A few concrete triggers for a duplicate delivery:

  • Your endpoint received the request and started processing it, but took too long to respond with a 2xx status before Stripe's timeout -- Stripe assumes the delivery failed and retries, even though your server is still working on (or already finished) the first one.
  • A transient network blip between Stripe and your server causes the acknowledgment to get lost, even though the request itself landed.
  • Someone on your team manually resends an event from the Stripe Dashboard's webhook event log while debugging, not realizing the original delivery actually succeeded.
  • Stripe's own automatic retry schedule for failed deliveries (which can span hours) eventually succeeds on a later attempt, after an earlier attempt had already succeeded from the merchant's side for reasons that weren't visible to Stripe.

Any one of these can hand your webhook handler the identical event payload more than once, sometimes minutes apart, sometimes a day later.

The fix: make the handler idempotent by checking the event ID first

Every Stripe event carries a unique id field (e.g. evt_1NirD82eZvKYlo2C...). The fix is almost always the same shape regardless of what the handler actually does downstream:

  • Before doing anything else, check whether that exact event id has already been recorded as processed (a dedicated table, or even a unique constraint on a "processed events" column works fine at most volumes).
  • If it has, return a 2xx immediately and do nothing else -- this is the correct response, not an error state.
  • If it hasn't, process it, then record the event id as handled, ideally in the same transaction as the side effect itself (marking the order paid, decrementing inventory) so a crash between the two steps can't reintroduce the same gap.

Note that checking the underlying object's status (e.g. "is this Payment Intent already marked succeeded in our database?") is a reasonable secondary safeguard, but it's not a substitute for checking the event ID directly -- some workflows legitimately receive more than one meaningfully different event tied to the same object, and conflating "same object" with "same event" can mask real issues instead of just duplicate deliveries.

Stripe also doesn't guarantee events arrive in the order they occurred. A charge.refunded event can, in rare cases, be delivered before the payment_intent.succeeded event for the same payment has been processed by your system, especially under retry conditions. A handler that assumes strict ordering -- for instance, one that errors out or silently drops a refund event because it can't find a "succeeded" payment record yet -- will mishandle that case. Designing for "this event might arrive before a logically earlier one" from the start avoids a second, harder-to-diagnose class of bug on top of the duplicate-delivery issue.

Actionable considerations

  • Respond with a 2xx status as fast as possible -- do the actual processing (sending emails, calling other APIs) asynchronously after acknowledging receipt, rather than making Stripe wait on slow downstream work before you confirm you got the event.
  • Store processed event IDs with a reasonable retention window (Stripe's own retry window is bounded), not indefinitely, to keep the idempotency check cheap at scale.
  • Test this deliberately: Stripe's CLI (stripe trigger and stripe events resend) can resend a real event on demand, which is the easiest way to confirm your handler is actually idempotent rather than just assuming it is.
  • If you're triggering downstream automation in a tool like Zapier off a Stripe webhook rather than a custom handler, the same duplicate-delivery risk applies to that Zap -- confirm whatever step creates records or sends notifications has its own duplicate-check logic, since Zapier itself won't deduplicate Stripe's retries for you.

This pattern isn't unique to Stripe

The same "at least once, not exactly once" delivery model shows up across most webhook-based integrations, not just payments. If your business also syncs Shopify order events into another system, the identical idempotency discipline applies -- a retried order webhook can trigger a duplicate fulfillment record just as easily as a retried payment event can trigger a duplicate charge confirmation. Any time your business logic reacts to a webhook by creating or changing a record -- whether that's confirming a Stripe Payment Intent or updating an order somewhere else -- it's worth asking the same question: "what happens if this exact event arrives twice?" If the honest answer is "something gets duplicated," that's the trap, and the event-ID check above is almost always the fix.

For more implementation guides like this one, see the Consultants In A Box blog.

Want a hand with this?

Our consultants fix this kind of thing every week. Tell us what you're running into.

Free business analysis

Ready to get more from your software?

Start with the free business analysis. It reads your business, builds your personal plan and valuation, and then you can book a call with us.

Book a free consultation

Tell us about your team and the software you need help with. We'll follow up to schedule a time to talk.

Open the form on its own page

Talk to a person

The fastest way is to pick a time. Prefer to write? Leave a message and we will email you back, usually the same business day.

Book a free call

By sending this you agree to our Terms and Privacy Policy.