Engineering
Stripe is designed to retry webhooks when your endpoint doesn't respond in time. If your handler isn't idempotent, those retries become duplicate customer emails— and duplicate billing notifications erode trust faster than almost any other product mistake.
Stripe uses an at-least-once delivery model: it sends an event to your endpoint and expects a 2xx response within a short window. If the response is delayed—because your server is under load, a deploy is in progress, or a network hop is slow—Stripe treats the delivery as failed and retries. This is intentional and documented behaviour, not a bug.
Stripe will retry a failed webhook delivery multiple times over several hours, with exponential backoff. In a high-traffic period or during an incident, it is common for the same event to be delivered two to five times before your endpoint responds consistently.
For most types of webhook events, duplicate processing is a minor annoyance that results in a duplicate database record or a logged warning. For customer-facing billing notifications, the impact is more direct: the customer receives the same "your payment failed" email twice, sometimes minutes apart.
This reads as a product defect. Customers assume they are being charged twice, or that your system is broken. Support tickets follow. Unsubscribes from billing emails increase, which means future recovery attempts are less likely to land in the inbox. The technical problem has a measurable customer experience cost.
Stripe provides idempotency keys for API requests (so you don't double-charge a customer if a retry happens on the request side). For webhook handling, the idempotency mechanism is different: you track which event IDs you have already processed.
Every Stripe event has a unique id field (e.g. evt_1AbCdEfGhIjKlMnO). Before processing an incoming event, your handler checks whether that ID has already been seen. If it has, it returns 200 and exits without re-sending the notification. If it has not, it processes the event and records the ID.
This check needs to happen atomically, or with a short idempotency window, to handle concurrent duplicate deliveries. A database unique constraint on the event ID column is the simplest reliable implementation.
Payment failure recovery is a high-stakes context for idempotency because the failure events themselves are already emotionally charged for the customer. A single "your payment failed" email is expected and helpful. Two identical emails within five minutes look like a system error. Three look like spam. The negative perception compounds with each duplicate.
Recovery tools built without event-level deduplication pass this problem to merchants silently—and merchants often discover it only when a customer complains, by which point the trust damage is done.
See how idempotency applies to the broader failed payment recovery workflow and why it matters for dunning email delivery.
Idempotency built in
RenewalRescue tracks Stripe event IDs so duplicate webhook deliveries never produce duplicate customer emails—no extra engineering required on your end.
Get started free