skip to content

Several clients each retry a failed operation after the same fixed delay and keep colliding on every attempt, so almost none succeed. Explain what failure this is and why adding randomized jitter to the retry delay helps more than simply lengthening the delay.

level: seniorimportance: must knowfreq 55%

answer

  1. Identical delay preserves relative offsets → re-collide
  2. Longer fixed delay = fewer, not fairer, collisions
  3. Full jitter: random(0, min(cap, base·2^k))
  4. Jitter shrinks the contending population each round
  5. Cap + retry budget + circuit breaker, retry only idempotent work

basics

~20 s

It is a symmetric retry storm — a livelock. Identical delays keep the clients synchronised, so they re-collide every round no matter how long the delay. Jitter spreads their wake-up times, so one wins each round and the group de-synchronises instead of marching in lockstep.

solid answer

~60 s

This is a **retry storm / synchronised-collision livelock**. The clients are busy and runnable, but the retry schedule is deterministic and identical, so whatever offsets exist between them are preserved across rounds: they collide, all back off by the same amount, and arrive together again. Lengthening the delay only lowers the collision frequency, not the collision *probability* — the group stays in phase and each round still fails, so you trade a fast failure for a slow one. **Jitter breaks the symmetry.** If each client waits a random time drawn from an interval, arrivals spread out; with high probability one gets there first and completes, and the rest re-randomise. This is exactly what Ethernet's binary exponential backoff does and why modern retry guidance is exponential backoff *with* full jitter — `sleep = random(0, min(cap, base * 2^attempt))` — rather than the naive `base * 2^attempt`. Jitter also fixes a second, related problem: **thundering-herd re-synchronisation**. After a dependency outage or a cache expiry, all clients retry at the same instant and re-create the overload. Jitter smears that spike. Combine it with a retry budget, a cap, and a circuit breaker so retries cannot amplify load without bound.

code

text · 4 lines
text
no jitter    : d = min(cap, base * 2^attempt)          # synchronised
equal jitter : h = min(cap, base * 2^attempt) / 2
               d = h + random(0, h)
full jitter  : d = random(0, min(cap, base * 2^attempt))  # preferred

go deeper

for a junior

Name the failure (everyone retrying in lockstep) and state that randomizing the wait makes one client arrive first, so someone finally succeeds.

for a middle

Show the mechanism: identical delays preserve relative arrival offsets, so collision probability is unchanged; write the full-jitter formula and mention the cap and attempt limit.

for a senior

Discuss operating it: base delay versus service time, retry budgets, circuit breakers, retry amplification across layers, and idempotency requirements before retrying at all.

for a principal

Frame it as load-management strategy — retries are offered load, so the policy must be stable under saturation; combine jittered backoff with admission control, budgets and a fair queued fallback that guarantees eventual progress.

## The failure: synchronised retry When several independent parties contend for one resource and all fail, each schedules a retry. If the retry policy is deterministic and identical across parties, their *relative* arrival offsets are preserved. Two clients that arrived 1 ms apart and both sleep 100 ms will arrive 1 ms apart again — and if the resource requires more than 1 ms of exclusive use, they collide again. The system is fully occupied, the retry counter climbs, and completed work approaches zero. Because everyone is runnable and mutating state, this is livelock, not deadlock. The pattern shows up at every scale: threads retrying an optimistic compare-and-swap, transactions retrying after a serialization conflict, HTTP clients retrying a 503, nodes retrying a leader election, radios retrying a shared medium, and pollers that all fire on a wall-clock boundary such as the top of the minute. ## Why a longer fixed delay does not fix it Think of two variables: **collision probability per round** and **rounds per second**. A longer fixed delay reduces rounds per second, so you burn less CPU and network — a real but secondary benefit. It does not touch collision probability, because the parties remain in phase. Formally, with N parties whose retry times are a common deterministic function of the attempt number, the arrival process stays a shifted copy of itself, so the same subset collides forever until some external noise (GC pause, scheduler hiccup, packet loss) accidentally breaks the tie. Relying on accidental noise is why these incidents are erratic and hard to reproduce. Worse, a long fixed delay *creates* a new hazard: it aligns clients into a periodic spike. Every delay period the resource sees N simultaneous arrivals and zero traffic in between, which is the worst possible arrival distribution for a queueing system. ## Why jitter works Jitter converts a deterministic schedule into a random one. If each party waits `U(0, W)` for window `W`, then the probability that two specific parties land within the resource's service time `s` of each other falls roughly as `s/W`, and the expected number of parties in the first slot is about `N·s/W`. Choosing `W` proportional to `N·s` makes the first arrival almost always unique: one party wins, completes, and leaves; the rest re-randomise and the population drains. That is the key property — **jitter makes the contending population shrink**, whereas fixed backoff keeps it constant. There are three common formulations, given a base delay `b`, attempt number `k` and cap `c`: - **No jitter**: `d = min(c, b·2^k)` — synchronised, bad. - **Equal jitter**: `d = h + random(0, h)` where `h = min(c, b·2^k)/2` — keeps a growing floor while spreading arrivals. - **Full jitter**: `d = random(0, min(c, b·2^k))` — the usual recommendation; it minimises both collisions and total work in simulation, at the cost of occasionally retrying very soon. Decorrelated jitter (`d = min(c, random(b, prev·3))`) is a variant that avoids the very short delays of full jitter while still de-synchronising. ## Choosing the parameters - **Base delay** should be at least the resource's service time; retrying faster than the operation can complete guarantees collisions. - **Growth factor** of 2 is conventional; the point of growth is to shrink offered load as the contending population turns out to be large. - **Cap** bounds worst-case latency; without it exponential growth drives tail latency into timeout territory. - **Attempt limit or retry budget**. Per-request attempt limits are not enough — a global budget (for example, retries may not exceed 10% of primary requests) is what prevents a partial outage from being amplified into a full one. Above the limit, fail fast or escalate to a fair, queued path that guarantees progress instead of racing. - **Retry only idempotent or safely-retryable work**, and only on error classes where a retry can plausibly succeed. ## Related but distinct: the thundering herd Jitter also addresses re-synchronisation events that have nothing to do with retries: a cache entry that expires at the same instant for all clients, a dependency that recovers and admits everyone at once, cron jobs on the same schedule, or reconnect storms after a network blip. Spreading expiry times and reconnect delays randomly is the same medicine. Note the difference in intent: backoff jitter de-synchronises *contenders*; herd jitter de-synchronises *arrivals*. Both are symmetry-breaking. ## Alternatives and complements Randomisation is not the only way to break symmetry. A deterministic tiebreaker also works and is often stronger: give each party a priority (id, timestamp, transaction age) and let the winner keep waiting while the loser aborts. Database concurrency control uses exactly this with wound-wait and wait-die. Alternatively, remove the contention: replace optimistic retry with a fair FIFO queue, batch conflicting work behind a single owner, or shard the resource so parties rarely meet. In practice a robust design layers them — jittered backoff for the common case, a bounded budget, and escalation to a queued path that guarantees an eventual turn.

  • Full jitter can pick a very short delay right after a failure. Why is that acceptable, and when is it not?
    It is acceptable because the expected delay still grows with the attempt number, so offered load falls overall while individual clients occasionally get a quick second chance — which is good for latency. It is not acceptable when the downstream is failing due to overload and even a small burst of early retries prolongs the outage; there you want decorrelated or equal jitter with a floor, plus a circuit breaker that stops retrying entirely.
  • How do you stop retries from turning a partial outage into a full one?
    Cap the total retry load rather than only per-request attempts: a retry budget that allows retries only while they stay under a small fraction of primary traffic, plus a circuit breaker that trips on sustained failure and a token or concurrency limit on the retry path. Retries should also never be amplified across layers — if the client, the gateway and the service each retry three times you get 27 attempts, so pick one layer to own retries.
  • Give a symmetry-breaking technique that does not use randomness.
    Use a deterministic tiebreaker such as transaction age or node id. Database schemes wound-wait and wait-die let the older transaction win — the older either aborts the younger holder or waits, and the younger never waits on the older — which guarantees no cycle and no indefinite retry loop. Static lock ordering and single-owner batching are other deterministic ways to remove the symmetric conflict.

Two people phoning each other at the same moment get a busy signal; if both hang up and redial after exactly ten seconds they collide forever. If each waits a random few seconds, one gets through on the first try.

saying these in an interview costs you the question

  • "Just increase the retry delay" — a longer identical delay keeps the clients in phase and only slows the failure down.
  • Treating jitter as a cosmetic smoothing trick rather than the mechanism that shrinks the contending population.
  • Unbounded exponential backoff with no cap or attempt limit, driving tail latency past every timeout.
  • Retrying non-idempotent operations, turning a livelock discussion into duplicate side effects.
  • Stacking retries at several layers (client, proxy, service) so attempts multiply and amplify an outage.

context