WebhooksApril 18, 20268 min read

Stripe Webhook Retries: A Production Guide

Stripe retries failed webhooks for up to 3 days. Here is how the schedule works, what breaks when your endpoint is down, and how to design for it.

ATTEMPT SPACING · EXPONENTIALnow5m30m3h18hgive upstatus=failed · payload still in the log · still replayable
Three days of retries, then silence. Your own log is what decides whether that silence matters.

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 "before any complex logic that could cause a timeout." The community's working number is 20 seconds. A timed-out delivery counts as failed, as do 5xx responses and network failures.

On failure, Stripe retries the event, on exponential backoff for up to 3 days. Retries 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 can retry much harder: Paddle makes up to 60 attempts in about two 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 its unpublished timeout 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.

A bug returns 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 });
}
waitUntil · THE 200 IS A PROMISE YOU CANNOT KEEPverifyreturn 200waitUntil(work)in memory onlyrecycled → gone, never resentPERSIST FIRST · THE 200 IS TRUEverifydurable writereturn 200processretryable
Return 200 on something you have durably written down, and the provider's clock stops being a constraint on your business logic.

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 stops retrying after 3 days and keeps events for 30, so past that 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 about a second, so the response window is never a problem
  • Log every event with full headers and body, encrypted at rest
  • Retry delivery to your server at 5 minutes, 30 minutes, 3 hours, 18 hours, then daily: 3 attempts on Free, 5 on Pro, 10 on Scale. Stripe's own schedule runs for up to three days, so on Free and Pro the long safety net is the stored log and replay, not the retry count
  • Give you one-click replay. If your server was down for an hour, the app page replays that hour's failed events in one press (up to 100 at a time), and replays of failed events don't count against your monthly quota, though they have a daily limit and use your compute allowance
  • Alert on failure streaks. We email you at 1 and 5 consecutive failures. At 20 we pause delivery to that destination (10 minutes at first, longer on repeat trips) and email again; events that arrive during a pause are logged for replay rather than delivered

No SDK, one URL swap. The first delivery still passes your existing Stripe check. Retries and replays carry Stripe's original timestamp, which the Stripe SDK rejects after five minutes, so check the AnyHook-Signature header to accept those.

To see it before signing up for anything, one command returns a live inbound URL with no account:

curl -X POST https://anyhook.net/api/v1/quickstart

The response carries an inbound_url to point a provider at, an api_key for reading the event log over the API, and a claim_url. The endpoint is temporary: it and its events are deleted after 7 days unless you open the claim URL and sign in, which keeps the same URL.

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.

All postsApril 18, 2026 · 8 min

Stop losing webhooks.

Change one URL. Get retries, an event log and one-click replay. Or see it first: sign up and run the 30-second outage drill, and watch an event survive your server being down. No server needed.