Why Stripe webhook duplicates are a documented certainty, not a bug, and the three-part pattern that makes billing survive replay.

Stripe will send you the same webhook event more than once. Not as an edge case, it's documented behavior, they say so plainly in their own docs, delivery isn't exactly-once and order isn't guaranteed.
Most handlers get written like it is anyway.
I found this out at Loom the hard way, and not through a stack trace. It was a support message asking why a customer got two "you're now on the Pro plan" emails ten minutes apart. Nothing crashed. Nothing errored. Two separate webhooks, customer.subscription.updated and invoice.paid, both fired for the same upgrade close enough together that each one thought it was the only thing finalizing the change. The handler looked correct in isolation. It broke under replay.
That's the bug that doesn't show up in a demo. It shows up three weeks later in your support queue, or in finance asking why MRR doesn't match the Stripe dashboard.
And it's not one problem, it's two, stacked. Event idempotency (the same event running once) and business idempotency (the same outcome applying once) are separate failure modes. You can dedupe the event id perfectly and still double-send, because Stripe fires multiple event types for one business outcome. payment_intent.succeeded and invoice.paid can both think they're the one closing out a checkout.
Wrote up the full pattern I use now: one durable ledger with a unique constraint on event_id, a fast Redis lock for the common case, and an ownership rule where exactly one function is allowed to execute any given side effect (email, plan swap, session close), everyone else just reconciles state.
Twenty extra lines of code and one schema constraint. Cheap to build right the first time, expensive to discover after a refund processes twice.
Full pattern, code, and the test that proves it 👉 Full article
0
0
0