Yoann Jaspar

Jul 23, 2026 • 2 min read

Idempotent Stripe Webhooks in Node.js TypeScript: Patterns That Survive Replay

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

Idempotent Stripe Webhooks in Node.js TypeScript: Patterns That 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

Join Yoann on Peerlist!

Join amazing folks like Yoann and thousands of other builders on Peerlist.

peerlist.io/

It’s available... this username is available! 😃

Claim your username before it's too late!

This username is already taken, you’re a little late.😐

0

0

0