Webhook delivery isn't guaranteed on the first attempt. Networks fail, servers restart, deploys cause brief downtime. Providers know this — so they retry. Understanding retry behavior helps you design receivers that handle failures without double-processing or data loss.
Why Retries Happen
A delivery attempt fails when: your server returns a non-2xx HTTP status, your server doesn't respond within the timeout window (typically 5-30 seconds), or a network error prevents connection.
Provider Retry Schedules
| Provider | Max Attempts | Window | Strategy |
|---|---|---|---|
| Stripe | ~15 | 3 days | Exponential backoff |
| GitHub | 10 | 3 days | Exponential backoff |
| Shopify | 19 | 48 hours | Increasing intervals |
| Twilio | 3 | ~4 hours | Linear |
| SendGrid | 3 | 72 hours | Exponential |
| PagerDuty | No retries | — | Fire and forget |
Idempotency: Your Safety Net
Because retries mean duplicate deliveries, your handler must be idempotent. Every provider includes an event ID in the payload — store it and check before processing.
async function handleWebhook(eventId, payload) {
const exists = await db.query(
'SELECT 1 FROM processed_events WHERE event_id = $1', [eventId]
);
if (exists.rows.length > 0) return;
await db.transaction(async (tx) => {
await tx.query('INSERT INTO processed_events (event_id) VALUES ($1)', [eventId]);
await processEvent(payload, tx);
});
}Respond Quickly, Process Async
Return 200 immediately. If your handler takes 30 seconds before responding, you'll hit the timeout and the provider will retry even though you eventually succeeded.
Dead-Letter Queues
When all retries are exhausted, the event is permanently dropped. For critical events (payments, subscription cancellations), implement a dead-letter queue.
CREATE TABLE webhook_dlq (
id SERIAL PRIMARY KEY,
provider TEXT NOT NULL,
event_id TEXT,
payload JSONB,
error TEXT,
failed_at TIMESTAMPTZ DEFAULT now()
);Summary
- Return 2xx fast — don't let processing delay your response
- Store event IDs and check before processing (idempotency)
- Monitor your webhook endpoint — alert on sustained failures
- Implement a dead-letter queue for critical events