Retries, backoff and jitter
What to retry, how long to wait, and how many retries to allow. One page, for quick lookup.
The rule
Retry only what an identical later request could fix, only if the operation is idempotent (a timeout may already have done the work), at one layer, with capped exponential backoff + full jitter, under a retry budget, within the deadline.
What to retry
Error classes from labs/payflow/lib/errors.ts.
| error | meaning | retry? |
|---|---|---|
TimeoutError | stopped waiting; outcome unknown | yes, only if idempotent |
TransientError 503 | refused before any work | yes, after a wait |
ConflictError 409 | same key still in flight | yes, after a wait |
ValidationError 400 | the request is wrong | no |
MismatchError 422 | key reused with other params: a bug | no |
General HTTP: "request is wrong" (400, 401, 403, 404, 422) → don't retry. "Try later" (408, 429 with Retry-After, 503, and 409 only when it means "same idempotency key still in flight") → retry after waiting. A general 409 Conflict (the resource's state conflicts with the request) usually fails again. 500/502/504 and timeouts: unknown outcome → retry only if the call is idempotent. Check each API's contract: Stripe treats every 5xx as indeterminate, and its Stripe-Should-Retry header can say "retry" or "don't" outright.
The formula (full jitter)
// attempt = 0 before the first retry
ceiling = min(capMs, baseMs × 2^attempt)
wait = random() × ceiling // uniform in [0, ceiling]
Ceilings with base 10 ms, cap 2000 ms, for attempt 0, 1, 2…: 10 → 20 → 40 → 80 → 160 → 320 → 640 → 1280 → 2000 → 2000 ms.
The retry loop: stop checks, in order
| question | why here | |
|---|---|---|
| ① | out of attempts? | free, no shared state |
| ② | can a retry fix this error? | free; must come before ③ so a 400 never spends a token |
| ③ | does the budget allow a retry? | spends a token |
| ④ | would the jittered wait cross the deadline? | sleep the same wait you checked |
Any "stop" → rethrow the last error object itself, not wrapped, so the caller can still tell a timeout from a 503. Every success, including a first try, refills the budget.
Retry budget (token bucket)
| event | tokens |
|---|---|
| start | capacity (lab: 10) |
| retry | −1 (no token → don't retry) |
| success | +perSuccess (lab: 0.1), never above capacity |
A retry needs a whole token (≥ 1). Float drift: 0.1 added ten times is 0.9999999999999999, so round when adding (or count whole tenths).
Steady state ≈ 1 retry per 10 successes. Google SRE example of a server-wide budget: 60 retries per minute per process.
Amplification
k attempts per layer, n layers → kⁿ at the bottom. Lesson: 3 layers × 3 attempts → 27. Brooker: five layers, "the load on the database will increase 243x" (he says "three retries" per layer but counts 3 tries each: 3⁵ = 243). Google SRE: three layers that "all issue 3 retries (4 attempts)" → "64 attempts (4^3)". Fix: retry at a single point in the stack.
Lab numbers (npm run l3, simulated, seed 1)
500 clients, capacity 50 per 10 ms tick, each 503 costs 0.1 of a slot, max 10 attempts.
| policy | total calls | wasted (503) | gave up | ms until done |
|---|---|---|---|---|
| immediate | 4722 | 4648 | 426 | 100 |
| exponential, no jitter | 4722 | 4648 | 426 | 4560 |
| full jitter | 2274 | 1774 | 0 | 260 |
Immediate and no-jitter exponential tie exactly because the simulation has no natural spread: identical waves meet an identical server. It's a worst case. The collapse also depends on rejections costing work: with REJECT_COST=0 or CAPACITY=100, immediate retry finishes faster than jitter (jitter still makes fewer calls). Among jittered variants, Brooker calls equal jitter "the loser"; full vs decorrelated "is less clear".
Vocabulary
| term | meaning |
|---|---|
| retry | Sending the identical request again after a failure. |
| backoff | Waiting longer before each new attempt. |
| jitter | Randomness in the wait, so clients don't retry in lockstep. |
| thundering herd | Many clients retrying at the same moment, overloading a recovering server. |
| retry budget | A cap on retries relative to successes (a token bucket). |
| retry amplification | Retries at several layers multiplying the load at the bottom. |