A leading product engineering company, creating adaptive software solutions to improve operations, providing businesses with expert development services from across domain.
A leading product engineering company, creating adaptive software solutions to improve operations, providing businesses with expert development services from across domain.
Stripe documents no ordering guarantee, duplicate deliveries and three days of retries. Webhook retry patterns that survive all three.

Stripe's documentation says, in plain words, that it does not guarantee the delivery of events in the order they are generated. It also says an endpoint might occasionally receive the same event more than once, and that two separate Event objects can be produced for a single underlying change.
Most webhook handlers we are asked to fix violate at least two of those. Not because the teams were careless, but because the defects are invisible until traffic is high enough for the improbable to happen daily. Webhook retry patterns are one of those areas where the code passes review, passes tests, and is still wrong.
Creating a subscription can generate customer.subscription.created, invoice.created, invoice.paid and charge.created. They can arrive in any order.
The documentation goes further than most people notice: snapshot events record created in seconds, so distinct events can share a timestamp, and you are told explicitly not to use created to determine order or to decide whether you have already processed something. If your handler sorts by timestamp to reconstruct sequence, it is sorting a field that cannot do that job.
The correct model is that each event describes a state you can verify, not a step in a sequence you must replay. When an invoice.paid arrives before the invoice you expected to create first, the answer is to fetch the invoice rather than to queue the event and wait.
Duplicates happen. The documented mitigation is to log the event IDs you have processed and skip ones you have already seen. For the case where two distinct Event objects describe the same change, deduplicate on the object ID in data.object together with the event type.
The subtlety teams miss is where the deduplication happens. Checking "have I seen this ID" and then processing is two operations, and between them your other worker is doing the same thing with the same event. Under retry storms — exactly when duplicates are most likely — that race fires constantly. The check and the claim have to be one atomic operation: a unique constraint on the event ID, an upsert that fails on conflict, or a transaction. An in-memory set is not deduplication, it is optimism.
Stripe retries for up to three days with exponential backoff in live mode. That is a long time for a bug to keep arriving. In a sandbox it retries three times over a few hours, which means the behaviour you observe while developing is not the behaviour you get in production.
Two consequences. A handler that throws on a malformed payload will keep receiving that payload for three days. And an endpoint that was briefly down does not simply miss events — it receives a compressed burst of them later, out of order, possibly duplicated, at a moment when it may still be recovering.
Redirects count as failures. A 302 is treated as a failed delivery. If your webhook URL redirects from the apex to www, or http to https, or has a trailing-slash rule, every delivery fails and retries for three days. This is the single most common cause of "we are not receiving webhooks" that we see, and it never appears in application logs because the request never reaches the application.
Doing the work before responding. The endpoint must return a 2xx quickly, before any logic that could time out. The correct shape is: verify signature, persist the raw event, return 200, process asynchronously. Teams that process synchronously discover this during their first traffic spike, when the queue of renewals at the start of the month overwhelms the handler and the timeouts trigger retries that add more load.
Framework middleware breaking signature verification. Verification needs the raw body. Any framework that parses and re-serialises JSON before your handler sees it will produce a body that no longer matches the signature. The fix is per-route raw body handling, and the symptom — verification failing for no apparent reason — sends people looking at their secret instead.
CSRF protection on the webhook route. Rails, Django and others check a token on every POST. A third party cannot supply one. The route needs an explicit exemption.
Subscribing to everything. It is tempting to register for all event types and filter in code. Stripe recommends against it, and the practical cost is that your endpoint absorbs traffic it will discard, at exactly the volumes where you least want the load. Subscribe to the types you handle and add new ones deliberately.
The signature header carries a timestamp and one or more signatures, computed as HMAC-SHA256 over the timestamp, a full stop, and the raw body, keyed with the endpoint secret. Three details matter and are routinely skipped.
v0 scheme is also sent; accepting arbitrary schemes is how downgrade attacks work.Note also that a retry gets a fresh timestamp and signature, so a replayed delivery from two days ago is legitimately signed and legitimately recent. Signature verification does not deduplicate for you.
One more, specific to organisations and Connect platforms: a handful of event types expect a response rather than being fire-and-forget, and those cannot be handled by an organisation-level destination at all. If authorisation decisions or checkout redirect behaviour depend on your reply, that endpoint has to live on the account itself. This is the kind of constraint that only surfaces when a platform scales into multi-account structure, long after the handler was written.
Five steps, in this order:
This is not sophisticated. It is four lines of defensive code that most integrations skip because none of the failure modes appear in the happy path, and all of them appear at month end.
When something does go wrong for long enough that retries expire, you need a way back. Stripe allows a manual resend for up to 15 days after event creation from the dashboard, and up to 30 days via the CLI. Those two numbers being different has caught out more than one team at the two-week mark.
Worth knowing: manually resending an event does not stop the automatic retry schedule, even if your endpoint returns a 2xx. If you are replaying a backlog by hand while the platform is also retrying, you will receive each event more than once — which is fine if you built the deduplication above, and a second incident if you did not.
The more durable answer is to reconcile rather than replay: periodically list objects changed since your last known good point and compare against your own records. Webhooks are an optimisation over polling, not a replacement for being able to poll. Every integration that has run for more than a year eventually grows this job, usually after the incident that made it obvious.
Payments and payroll, because both reconcile. A duplicate invoice.paid that credits an account twice is not a logging problem, it is a financial one, and it will be found by a customer rather than by you.
We built the integrations behind an enterprise payroll platform, where a payment event processed twice means somebody is paid twice and the reconciliation runs the following month. The defensive work above is the difference between an incident and a non-event, and it costs a day.
If you are wiring up third-party integrations now, the cheapest possible test is worth running before launch: send the same event to your endpoint twice, in the wrong order, and see what your database looks like afterwards. Most handlers fail that test, and the ones that fail it silently are the expensive kind.
If yours does, it is a day of work to fix and considerably more to unpick after the fact.
No. Stripe states events are not delivered in the order they are generated, and that the created timestamp must not be used to determine order or detect duplicates, because distinct events can share a timestamp.
Up to three days in live mode with exponential backoff. In a sandbox it retries three times over a few hours, so development behaviour does not match production behaviour.
Insert the event ID into a table with a unique constraint and treat a conflict as already-processed. Checking then processing is a race that fires exactly during retry storms, when duplicates are most likely.
Most often a redirect. Stripe treats a 3xx as a failed delivery, so an apex-to-www or http-to-https rule fails every attempt before the request reaches your application, producing no application logs at all.
The default five minutes. Setting the tolerance to zero does not make it stricter — it disables the recency check entirely, removing the replay protection the timestamp exists to provide.
Ready to take the first step towards unlocking opportunities, realizing goals, and embracing innovation? We're here and eager to connect.
11th Floor, O-Hub, Chandaka Industrial Estate, Infocity, Bhubaneswar, Odisha 751024
Level 4, 11 York Street Sydney Startup Hub Sydney, NSW – 2000
30 N. Đinh Nghệ, Phước Mỹ Sơn Trà, Đà Nẵng / Da Nang City – 550000
Level 25, AIDP Business Tower, Dubai Marina, United Arab Emirates
50 Beauchamp Street, Wellington, WGN 5028, New Zealand