Article · Updated November 1, 2026 · By the Hooks team

Webhook retry strategy that works

Short answer: A workable webhook retry strategy retries only transient failures (timeouts, 429, 5xx), backs off exponentially with jitter, keeps the same delivery id so the receiver can deduplicate, and stops after a bounded window or attempt count. Permanent failures (400, 401, 404) should not be retried at all.

Retries protect against blips. Unbounded retries turn one broken destination into a queue that never drains.

Classify the response first

2xx: delivered. 3xx: follow only if the client follows redirects safely, otherwise treat as failure. 408, 425, 429, 5xx: retry. Other 4xx: fail immediately and surface the error, because resending the same request will not help.

Back off, with jitter

Double the delay between attempts and cap it, then add randomness so thousands of deliveries to a recovering destination do not arrive at the same instant. Honour Retry-After when the destination sends it.

Delay

delay = min(cap, base * 2 ** attempt)
delay = random(0, delay)        // full jitter
if (retryAfter) delay = max(delay, retryAfter)

Keep identity stable and bound the window

Every attempt for one event carries the same delivery id, which is what lets the receiver deduplicate a request whose response was lost. After the window closes, move the delivery to a dead-letter state with its failure reason, and replay it once the destination is fixed.

Frequently asked questions

How many retries are enough?

Bound by time rather than count. Most incidents clear within minutes to hours, so a window of a few hours to a day covers the useful cases without holding a broken destination forever.

Should a 429 count as a failure?

It is a retryable failure. Back off at least as long as Retry-After says, and count it toward the attempt budget so one throttling destination cannot stall the queue.

Keep reading