Skip to content
Engineering

The Webhook Retry Runbook for Reliable Delivery

BYOB Team

BYOB Team

8 min read

Webhook providers retry failed deliveries, so endpoints must be idempotent and fast. Verify signatures over the raw body, persist the event, acknowledge in milliseconds, process asynchronously, retry transient failures with capped backoff plus jitter, and park exhausted events in a dead letter queue with full replay context.

Key takeaways

  • • Acknowledge fast and process asynchronously, since slow endpoints cause redelivery storms
  • • Verify HMAC signatures over the raw body and check timestamp tolerance before trusting anything
  • • Retry transient failures with capped backoff plus jitter, then park exhausted events in a dead letter queue
  • • Tie handlers to provider event IDs so replays converge instead of double applying
The Webhook Retry Runbook for Reliable Delivery

How do you design webhooks for redelivery? #

Treat every webhook like a delivery driver who keeps knocking until someone signs. That is literally the contract. Providers retry, events duplicate, order scrambles, bursts arrive during incidents.

Every webhook endpoint will receive duplicates, out of order events, and bursts during provider incidents. The runbook response is a fixed shape: verify the signature, persist the event, acknowledge fast, process asynchronously, and make handlers idempotent by event ID. Slow or stateful request handlers turn one failure into a redelivery storm.

Keep two dashboards: delivery health with attempts, latency, and failure rate by endpoint, and business outcomes with orders fulfilled and entitlements granted. When they diverge, the bug sits in the handler, not the network.

flowchart LR A[Event fails] --> B[Retry with backoff] B --> C{Success} C -->|Yes| D[Mark done] C -->|No| E[Send to dead letter queue] E --> F[Alert and replay]

How do you acknowledge fast and process asynchronously? #

The request handler has one job: decide accept or reject in milliseconds. Verify the HMAC signature over the raw body, check the timestamp tolerance window, persist the raw event with its provider event ID, and return success. Everything else, entitlement updates, emails, provisioning, runs in a background worker reading from a queue.

Stripe documents this shape clearly: receive events, verify signatures, and return a fast success response, with slow work handled outside the request (https://docs.stripe.com/webhooks). GitHub describes the same push model where deliveries expect prompt responses and get retried on failure (https://docs.github.com/en/webhooks). Paddle frames its webhook events as notifications your app must accept quickly and reconcile (https://developer.paddle.com/webhooks/overview).

Slow handlers cause the exact failures they fear. A handler that calls three downstream APIs before responding will time out under load, and the provider will redeliver, doubling the downstream calls. Fast ack plus queue converts redelivery from a correctness threat into a duplicate the idempotency check absorbs.

js
// Minimal webhook handler shape
export async function POST(req) {
  const raw = await req.text();
  if (!verifyHmac(raw, req.headers)) return Response.json({ ok: false }, { status: 400 });
  const event = JSON.parse(raw);
  const fresh = await storeEventOnce(event); // unique on provider event id
  if (fresh) await enqueue('webhook.work', { id: event.id });
  return Response.json({ ok: true }); // always fast
}

Signature verification deserves care. Read the raw body before any JSON parsing, since parsing can alter whitespace and break the digest. Compare digests in constant time. Reject events outside the tolerance window even when the signature matches, because captured payloads stay replayable forever without that check. Stripe documents signature verification with timestamp tolerance as the expected defense (https://stripe.com/docs/webhooks/best-practices).

Step Job in request Job in worker Dedupe key
Verify HMAC over raw body plus clock skew check No No
Persist Store event once No Provider event id
Ack fast Return 2xx in ms No No
Process No Entitlement update async Logical event id plus delivery id log

How do you handle retries, backoff, and the dead letter queue? #

Providers retry on their own schedule, but your outbound webhooks and your internal reprocessing need explicit policy. Retry transient failures like timeouts, 5xx responses, and rate limits with capped exponential backoff plus jitter. Do not retry validation failures, signature mismatches, or missing entity errors on a timer. Those need human or code fixes, and timers just burn quota.

A workable schedule looks like this: retry after seconds, then minutes, then tens of minutes, with a cap around a day and jitter so a fleet of failures does not march in lockstep. Jitter matters more than people think. Without it, every failed event from an outage retries at the same second and recreates the outage.

After a bounded number of attempts, park the event in a dead letter queue with full context: payload, headers, attempt history with status codes, and the handler version that failed. Review the dead letter on a cadence, fix the root cause, and replay from the queue rather than asking the provider to resend blindly. Alert on dead letter growth rate in addition to depth. A sudden spike means a deploy broke the handler. Depth tells you about yesterday. Growth rate tells you about right now.

Try it: JSON Formatter and Validator

Try it right here: json formatter validatorOpen full tool

Loading the interactive tool… or open it here.

How do you make handlers idempotent by event id? #

Every handler must answer one question: what happens if this event processes twice. The answer should be nothing the second time. Key processing on the provider event ID with a uniqueness constraint, check durable state before applying side effects, and design state transitions so replays converge, moving pending to active and never active back to pending.

Separate delivery identity from event identity. One logical event can arrive with several delivery IDs across retries. Store both, dedupe on the logical ID, and log the delivery ID for tracing. Return success for already processed events rather than errors. Errors invite more redelivery of work already done.

sql
-- Dedupe on the logical event id, trace on delivery id
CREATE TABLE webhook_events (
  event_id TEXT PRIMARY KEY,
  delivery_id TEXT NOT NULL,
  payload TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'received'
);

State machine design does quiet work here. If fulfillment moves an order from pending to active, a replayed event finds active and stops. If your code instead toggles or appends, replays corrupt. Convergent transitions plus a uniqueness constraint make at least once delivery safe by construction.

How do you monitor without drowning in noise? #

Track attempts, success rate, ack latency percentiles, and queue depth per endpoint. Alert on sustained failure rate changes and dead letter growth, not single failures. Correlate spikes with deploys. Most webhook incidents follow a handler change, not a provider outage.

For billing webhooks specifically, add a reconciliation job. Poll the provider periodically and compare against locally recorded entitlements. The poller catches anything the push path missed, and the push path keeps latency low. Speed plus a backstop beats either alone. Paddle and Stripe both document event types and list APIs that make this reconciliation straightforward (https://developer.paddle.com/webhooks/overview).

One more habit. Log the handler version with every processed event. When a bad deploy slips through, you can name every event it touched and replay exactly those, instead of guessing from timestamps.

Build the endpoint boring. Verify, persist, ack, queue, dedupe, reconcile. The driver keeps knocking. Your door should open fast and remember every knock.

What are the trade-offs? #

Design for redelivery wins when providers promise at least once. Stripe and Paddle retry on timeouts and 5xx, so verify HMAC over the raw body, persist the event, ack in milliseconds, work async, retry transient failures with capped backoff plus jitter, and park the exhausted in a dead letter queue keyed by provider event id.

Pick this runbook when Pick polling when
Volume or freshness needs push delivery The provider allows pulls and volume is low
Duplicates must converge through idempotent handlers Events are rare enough that a cron covers them
Replay with full context must be possible Ordering guarantees matter more than latency

It loses when billing logic lives inside the request handler or monitors drown in noise. Slow endpoints cause redelivery storms. Pick the alternative shape: fast ack, queued work, reviewed dead letters.

Who this is for (and who should skip it)? #

This runbook helps builders making webhook endpoints survive redelivery, ordering drift, and bursts. If you own billing or provisioning via webhooks, the fast ack plus queue plus idempotency shape is load bearing.

Skip deep webhook infrastructure if you consume no webhooks and you reconcile by polling. That path stays simple until a provider requires push for lifecycle events.

One limit to know. Validation and signature errors should not retry on a timer, since repeats change nothing until code or config is fixed. Pair push handling with periodic reconciliation, or missed events stay missed.

  • Best for developers designing webhook endpoints for redelivery with fast acks and queued work.
  • Best for startups adding backoff, dead-letter review, and idempotent handlers by event ID.
  • Best for freelancers monitoring delivery health and business outcomes separately for clients.

What we learned building this? #

BYOB follows the verify plus persist plus fast ack plus queue plus dedupe shape for webhooks documented in the Paddle webhooks overview and Stripe webhooks best practices linked in the post, and the retry and queue wiring is collected in the BYOB deployment pipeline guide and billing handling in the BYOB Paddle payments guide, both verified 200 live. Handler versions are logged with each event so a bad deploy maps to an exact replay set instead of time based guessing.

How we picked these

Compared ack, retry, and dead letter advice to Stripe, GitHub, and Paddle webhook docs; no webhook deliveries were fired.

Frequently asked questions

Why do webhooks arrive more than once?

Providers guarantee at least once delivery. Timeouts, 5xx responses, and slow endpoints all trigger redelivery of the same event, as documented in the Stripe webhooks guide (https://docs.stripe.com/webhooks). Design handlers to absorb duplicates.

How should a webhook endpoint respond?

Verify the signature, persist the event, return success fast, and do slow work asynchronously. Stripe documents signature verification and fast acknowledgement as the expected shape (https://stripe.com/docs/webhooks/best-practices). Never run billing logic inside the request handler.

What belongs in a dead letter queue?

Events that exhausted retries, failed signature checks repeatedly, or reference missing entities, each stored with payload, headers, attempt history, and a replay path.

Changelog

  • • Added fit guide, comparison table, and hands on notes
  • • Added trade-offs section plus question form H2 pass (Sep 2026)
  • • House voice cleanup Sep 2026: removed negative parallelism cliches in prose

About the Author

BYOB Team

BYOB Team

The creative minds behind BYOB. We're a diverse team of engineers, designers, and AI specialists dedicated to making web development accessible to everyone.

Ready to start building?

Join thousands of developers using BYOB to ship faster with AI-powered development.

Get Started Free