Stripe webhook timeout: acknowledge first, work later
Stripe records an attempt as timed out when your endpoint takes too long to answer. The fix is not a faster server — it is returning 2xx before the slow work, then processing off the request path.
Works with Stripe webhook events. In the Stripe dashboard the attempt reads Timed out — “the destination server took too long to respond
to the webhook request.” There is no response code to read, because your handler never answered in
time.
What Stripe actually requires
Stripe does not publish a fixed timeout in its documentation. What it does state is the rule that
matters: your endpoint “must quickly return a successful status code (2xx) prior to any complex logic that could cause a timeout.” Treat the acknowledgement and the
processing as two separate things, and the exact number stops mattering.
Why a timeout costs you twice
- The event looks failed — Stripe saw no
2xx, so it queues a retry even though your handler may have finished the work seconds later. - You get the event again — Stripe retries for up to three days with exponential back off in live mode. A slow handler that eventually succeeds will therefore process the same event more than once. See retries and idempotency.
- Load compounds — spikes (monthly subscription renewals, for instance) arrive while the retries of the previous slow batch are still landing.
The fix: acknowledge, then work
Verify the signature, persist the raw event somewhere durable, answer 200, and hand the real work to a queue. Stripe's own guidance is to “configure your handler to
process incoming events with an asynchronous queue”.
// Before: everything happens inline, and the ack waits for all of it.
app.post('/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
await chargeLedger(event); // network call
await sendReceiptEmail(event); // network call
await syncToWarehouse(event); // slow
res.sendStatus(200); // …far too late
});
// After: acknowledge, then work.
app.post('/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
await events.insert({ id: event.id, payload: req.body }); // durable, fast
res.sendStatus(200); // ack immediately
queue.add('stripe-event', { id: event.id }); // then process
}); Find the slow part
- Outbound calls in the handler — email, ledger, analytics, or a third-party API. Each one adds its own latency and its own failure mode.
- Cold starts — a serverless function that boots on the first delivery of the day can spend most of the budget before your code runs.
- Lock contention — several events for the same customer arriving together and queueing on one row lock.
- Body parsing on the wrong path — signature verification needs the raw bytes; re-reading or re-serialising a large payload is avoidable work. See Stripe signature verification.
Confirm it in HookWatch
Point the Stripe endpoint URL at a HookWatch endpoint and every attempt is captured with the request, your target's response, and the time it took. A timeout stops being a one-word status in someone else's dashboard: you can see which event types are slow, how long your handler actually took, and whether the retry that followed was processed twice. Then replay the captured delivery once the handler is quick again.
Start debugging your webhooks.
Point one endpoint at HookWatch, capture a failure, and replay it once it’s fixed. Free during beta.