Engineering
Stripe webhook idempotency: why duplicate events happen and how to handle them
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.
Why Stripe retries webhooks
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.
The duplicate email problem
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.
Idempotency keys vs event-level deduplication
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.
Why this matters specifically for recovery workflows
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.
Related reading
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 freeFrequently asked questions
- What is webhook idempotency?
- An idempotent webhook handler produces the same outcome regardless of how many times the same event is delivered. For billing notifications, this means a customer receives at most one email per failure event—even if Stripe delivers that event two or three times due to network issues or retry logic.
- Does Stripe guarantee at-most-once webhook delivery?
- No. Stripe guarantees at-least-once delivery, which means the same event can be delivered more than once. This is standard behaviour for reliable event systems and is documented in Stripe's webhook documentation. Your handler must be idempotent to handle this correctly.
- What happens if I process the same Stripe event twice?
- Without idempotent handling, processing a duplicate event can trigger duplicate customer emails, double-counted metrics, redundant API calls, or inconsistent state in your database. For payment failure notifications, duplicate emails are the most visible problem—customers see them as spam and lose trust.
- How does RenewalRescue handle webhook idempotency?
- RenewalRescue tracks Stripe event IDs so that a duplicate event delivery does not produce a duplicate customer email. If the same failure event is received more than once—as happens when Stripe retries a webhook that did not receive a timely response—the duplicate is recognised and the notification is suppressed.