Exponential backoff is a retry strategy in which the wait before each new attempt grows exponentially, usually doubling: 1 second, then 2, 4, 8, 16, up to a maximum delay. It gives a struggling service room to recover instead of hammering it while it's down. In practice it's almost always combined with jitter (randomness) and a cap on the delay and the number of attempts.
delay(n) = min(cap, base × 2ⁿ)
with full jitter: sleep(random(0, delay(n)))
base = 1s, cap = 60s
attempt: 1 2 3 4 5 6 7
delay: 1s 2s 4s 8s 16s 32s 60s
Why not just retry immediately?
Immediate or fixed-interval retries cause two problems:
- They add load at the worst moment. A server returning
503because it's overloaded gets more requests from every client retrying. - They synchronise clients. If 10,000 requests fail together and all retry after exactly 5 seconds, the server gets 10,000 requests in the same instant again. This is the "thundering herd".
Backoff fixes the first, and jitter fixes the second.
Example with full jitter
async function withBackoff(fn, { base = 1000, cap = 60_000, maxAttempts = 7 } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await fn()
} catch (err) {
if (attempt + 1 >= maxAttempts || !isRetryable(err)) throw err
const retryAfter = err.retryAfterMs // from a Retry-After header, if any
const backoff = Math.min(cap, base * 2 ** attempt)
const delay = retryAfter ?? Math.random() * backoff
await new Promise((r) => setTimeout(r, delay))
}
}
}
const isRetryable = (err) =>
err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' ||
err.status === 429 || err.status >= 500
Jitter strategies
| Strategy | Delay | Notes |
|---|---|---|
| No jitter | base × 2ⁿ | Clients stay synchronised |
| Full jitter | random(0, base × 2ⁿ) | Spreads load best, a good default |
| Equal jitter | d/2 + random(0, d/2) | Guarantees a minimum wait |
| Decorrelated jitter | random(base, prev × 3) | Grows from the previous delay |
AWS's architecture blog analysed these and found full and decorrelated jitter do the least total work.
What to retry, and what not to
- Retry: timeouts, connection resets, DNS failures,
429,500,502,503,504. - Don't retry blindly:
400,401,403,404,422. They'll fail the same way until something changes. - Honour
Retry-After. If the server says when to come back, wait at least that long. - Make retries safe. Retrying a
POSTthat half-succeeded can create duplicates unless the operation is idempotent, for example by sending anIdempotency-Keyheader.
Exponential backoff in webhooks
Webhook providers use backoff for webhook retries: early retries come quickly to cover blips, later ones are hours apart to cover outages. Real schedules are often hand-tuned rather than pure doubling. The Webhook Relay Outbound Gateway, for instance, retries after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours, up to 8 attempts within 48 hours.
For inbound webhooks, Webhook Relay's durable retries run fast retries for a handoff window (15 minutes by default), then retry with exponential backoff on the schedule you choose: Seconds (about 25 minutes total), Medium (about 16 hours) or Long (up to 30 days). Details are in the durable webhooks docs.
If the problem is a burst rather than an outage, backoff alone isn't enough. Throttling releases queued webhooks to your endpoint at a steady rate or concurrency, so it never sees the spike.
Common mistakes
- No cap. Without a maximum, the 20th retry waits 12 days.
- No attempt limit or deadline. Eventually give up and move the work to a dead-letter queue for inspection.
- Retrying at several layers. If the HTTP client, the SDK and your job queue all retry with backoff, one failure can become dozens of attempts.
- Backing off in the request path. Sleeping inside a web request ties up a worker. Schedule the retry in a queue instead.
