how to handle duplicate Stripe invoice webhook deliveries idempotently
Handles duplicate Stripe webhook deliveries idempotently for invoice events. Use when invoice handlers must survive Stripe's at-least-once delivery without double-processing. Not for signature verification.
TL;DR
Stripe may deliver the same event more than once, so key every handler on the event id: store processed event ids and skip repeats before doing any business logic. Make the business operations themselves idempotent too (upserts, not inserts; guarded state transitions). Test by replaying the same event twice and asserting a single effect.
Steps
- Create a processed-events store (table or cache) keyed by the Stripe event id.
Expected: Duplicates have somewhere to be recognized.
- At the top of each handler, check the event id; if seen, acknowledge and return.
Expected: Repeat deliveries exit before touching business logic.
- Write business effects idempotently: upsert records, guard transitions (only draft to finalized once).
Expected: Even a missed dedup cannot corrupt state.
- Handle the race: two deliveries arriving simultaneously need an atomic check-and-set.
Expected: Concurrent duplicates are safe.
- Expire old event ids after your retry window plus margin (e.g. 30 days).
Expected: The store stays bounded.
When to use
- Invoice webhooks sometimes process twice
- You see duplicate fulfillment or accounting entries
- You need at-least-once delivery safety
When not to use
- Events fail signature verification (different problem)
- The handler logic itself is wrong on first delivery
- You need exactly-once delivery (not offered; idempotency is the answer)
Compatibility
Stripe webhooks at-least-once delivery; any datastore for event ids. Applies to all event types.
Variant phrasings
### Stripe webhook duplicate event handling
### idempotent webhook handler Stripe
### same invoice webhook delivered twice
Root cause
Stripe retries deliveries until your endpoint acknowledges, and network ambiguity means a successful processing can still look like a failure to Stripe, triggering a redelivery. At-least-once is the contract; idempotency is your half of it.
Edge cases
- Different event types for the same invoice (created vs finalized) are not duplicates; dedup by event id, not invoice id
- Database transactions should wrap the dedup check and the effect together
- The 30-day event id expiry must exceed Stripe's maximum retry window
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_38X0Q6z9IaDoNbLHSqbpBA