When your server is down and Stripe fires a payment_intent.succeeded, what happens to the event? Does Stripe retry forever? For how long? And how do you reconcile the events you missed?
This is the single most common question I hear from developers integrating Stripe, and it matters because a webhook you fail to process is revenue your product can't act on. A customer just paid. Your fulfillment job never fires. Their dashboard never updates.
TL;DR
- Stripe retries for 3 days with exponential backoff, then stops forever
- Return 200 fast, do work async, log every event before processing, be idempotent
- If you don't have time to build all of this, a webhook relay gives you retry, log, and replay with a single URL change
How Stripe retries
Stripe expects your webhook endpoint to return a 2xx status code quickly. It does not publish an exact number, and its docs only say to return 2xx "prior to any complex logic that could cause a timeout." The figure everyone works to is 20 seconds. Anything slower counts as a failed delivery, as do 5xx responses and network failures.
On failure, Stripe retries the event, on exponential backoff for up to 3 days. The first retry lands within minutes, subsequent ones get increasingly spaced out, and the final window is around 72 hours from the original event.
After 3 days, Stripe marks the event as failed and stops retrying. It does not wake you up. It does not send a fallback email. The event is simply gone from your delivery queue, though you can still see it in the Stripe Dashboard under Developers → Events.
Stripe's curve is also on the gentle end. Merchant-of-record platforms retry much harder: Paddle retries 60 times in three days, and a handler that keeps up with Stripe's schedule can still drown in Paddle's.
If the failures continue past that, Stripe eventually disables the endpoint entirely and emails you a shutoff date. Recovering from that is a different job, and the events remain pullable from the Events API for 30 days: see Stripe Disabled Your Webhook Endpoint for the backfill sequence.
The three failure modes that bite
Your endpoint is slow. Stripe counts a response past roughly 20s as a failure. If your webhook handler synchronously writes to a slow database, syncs to Salesforce, and sends a customer email, you're flirting with the timeout on every event.
Your deploy window. Your server restarts during a deploy, misses about 10 events, and comes back up. Stripe retries those events, but only for 3 days. If you don't catch it, they silently die.
A bug eats 500s. You push a deploy that throws on a specific event type. Stripe retries, but keeps hitting the same error. After 3 days Stripe gives up and you have a quiet data gap.
In all three cases Stripe is doing the right thing. The failure is on your side of the pipe.
Four things production-grade integrations do
1. Return 200 fast, do the work async
Don't do heavy work in the webhook handler. Accept the event, write it to a queue or durable log, return 200, and process asynchronously. This keeps you far from the timeout and decouples delivery from business logic.
export async function POST(req: Request) {
const event = await verifyStripeSignature(req);
await queue.publish(event); // durable write
return new Response(null, { status: 200 });
}
2. Log every event before processing
Persist the raw payload and headers before you touch business logic. If your logic throws, you can replay from the log. Stripe only keeps failed-delivery events for 3 days, so after that window your log is the only thing that lets you recover.
3. Idempotency on your side
Stripe sends the same event.id on retries, so your handler has to be idempotent: if you've already processed evt_1H..., don't process it again. A simple pattern:
INSERT INTO processed_events (stripe_event_id, processed_at)
VALUES ($1, NOW())
ON CONFLICT (stripe_event_id) DO NOTHING;
If the insert returns 0 rows affected, you've seen this event before. Skip.
Idempotency is deeper than it looks once retries start arriving concurrently, or out of order. You Received the Same Webhook Twice covers the transaction boundary, the claim-then-commit pattern for side effects you can't roll back, and why version guards rather than deduplication are what fix out-of-order delivery.
4. Alerting on failures
If 5 events in a row fail, you want to know within minutes, not when a customer emails support. Track consecutive failures and page yourself.
Where AnyHook fits
AnyHook sits in front of your server and solves this at the transport layer. You point Stripe at in.anyhook.net/you/stripe instead of your origin, and we:
- Return 200 to Stripe in under 50ms, so the response window is never a problem
- Log every event with full headers and body, encrypted at rest
- Retry with exponential backoff for longer than Stripe does, with per-plan retry counts
- Give you one-click replay. If your server was down for an hour, replay the events from that hour with one click, and replays don't count against your quota
- Alert on failure streaks. We email you at 1, 5, and 20 consecutive failures, and auto-pause after 20 to stop retry storms
No SDK. No code changes. One URL swap.
Takeaway
If you're losing Stripe events today, the fix is usually not bigger retries. It's persistence before processing, so you always have something to replay from.