FDEInterviews logo
⚙️ System Design for AI in Production
Foundational

Retries, Exponential Backoff and Jitter

When a call fails on a transient error, retrying immediately is the worst thing you can do: it piles load onto an already-struggling service and synchronizes every client into a stampede. Exponential backoff spaces retries out, and jitter de-synchronizes the clients so they stop arriving in lockstep.

TL;DR: Retry transient failures, but back off exponentially (1s, 2s, 4s, 8s) so you stop hammering a service that is already in trouble, and add random jitter so a thousand clients do not all retry at the same instant. Cap the attempts and the total time, and only retry operations that are idempotent.

Why naive retries make things worse

A downstream service hiccups and returns 503. The obvious fix is to retry. The naive version retries immediately, in a tight loop. Now multiply that by every client in the fleet. The service that was briefly overloaded is now hit with the original traffic plus a flood of instant retries, which keeps it overloaded, which causes more failures, which triggers more retries. That feedback loop is a retry storm, and it routinely turns a five-second blip into a half-hour outage.

There is a second, subtler problem. If all your clients fail at the same moment (a deploy, a brief network partition) and they all wait the same fixed interval before retrying, they retry in perfect unison. Each retry wave is as synchronized as the failure that caused it. This is the thundering herd: the load never smooths out, it just arrives in synchronized spikes.

RETRY STORM vs JITTER (toggle)
0s
1s
2s
3s
4s
5s
6s
7s
8s
9s
10s
11s
12s
13s
14s
15s
16 clients all fail at once and retry with exponential backoff. With jitter, each picks a random moment inside its backoff window, so the load spreads out.

Exponential backoff and jitter

Backoff fixes the first problem by increasing the wait after each failure: 1s, 2s, 4s, 8s. This gives a struggling service room to recover instead of being kept on the floor. Jitter fixes the second by randomizing each client's wait so the herd spreads out across the window instead of stacking on the same tick.

A retry that helps rather than piles on 1 A call fails 503, timeout, or a reset 2 Is it transient? a 400 will fail again 3 Is it idempotent? if not, do not retry 4 Double the wait 1s, 2s, 4s, 8s 5 Add random jitter break the synchronization 6 Cap attempts and time both, not just one 7 Give up to a dead letter rather than forever Retrying a non-idempotent write is how one timeout becomes two charges. If you cannot make it idempotent, the honest answer is that it is not retryable. Backoff gives a struggling service room to recover instead of keeping it on the floor. Immediate retries turn a five-second blip into a half-hour outage. Without jitter, a fleet that failed together retries together. Each wave is as synchronized as the failure that caused it, so load arrives in spikes and never smooths out. A cap on attempts alone still allows a very long tail once the waits are doubling. Bound the elapsed time as well, because a caller is waiting.

Steps 2 and 3 are gates rather than stages: a retry that skips either of them is how a timeout becomes a double charge, or how a permanent error burns a retry budget it was never going to use.

rendering diagram…
import random, time

def call_with_retry(op, max_attempts=5, base=1.0, cap=30.0, deadline=60.0):
    start = time.monotonic()
    for attempt in range(max_attempts):
        try:
            return op()  # op MUST be idempotent: it may run more than once
        except Transient as e:
            if attempt == max_attempts - 1:
                raise
            backoff = min(cap, base * (2 ** attempt))   # 1, 2, 4, 8, capped
            sleep = random.uniform(0, backoff)          # full jitter
            if time.monotonic() - start + sleep > deadline:
                raise                                   # respect total timeout
            time.sleep(sleep)
    raise RuntimeError("retry loop exited without a result")  # defensive guard

The random.uniform(0, backoff) is "full jitter": each client picks a random point in [0, backoff], which spreads the herd most aggressively. Two guards stop the loop from misbehaving: a max attempt count and a total deadline, so you fail fast instead of retrying for ten minutes against a service that is plainly down. Note also that you only retry on transient errors. A 400 (bad request) or 401 (auth) will fail identically every time, so retrying it just wastes the budget.

Why retried operations must be idempotent

A retry means the operation might run more than once. If the first attempt actually succeeded but the response was lost on the way back, your retry runs it again. For a read that is harmless. For "charge the card" or "send the email" it is a double-charge. Anything you retry must be safe to run twice, which means an idempotency key or an upsert. Backoff and idempotency are a pair: backoff makes retries safe for the server's load, idempotency makes them safe for the data.

Why interviewers probe this

This separates people who have run distributed systems from those who have only called APIs. The reserved follow-up is almost always "what stops the retries from making the outage worse?", and the strong answer names backoff, jitter, a cap, and a circuit breaker that stops retrying entirely once a service is clearly down. A great candidate connects it back to idempotency unprompted, because that link is where the real production bugs live.

Common misconceptions

  • "Retry until it works." Unbounded retries against a down service are a self-inflicted outage. Cap attempts and total time.
  • "Backoff alone is enough." Without jitter, synchronized clients just stampede on a slower clock. You need both.
  • "Retry everything that fails." Only transient errors (timeouts, 503, 429) are worth retrying. A 4xx will fail the same way forever.
  • "Jitter is a micro-optimization." At fleet scale it is the difference between load that smooths out and load that arrives in cliffs.

Key takeaways

  • Immediate retries cause retry storms; fixed-interval retries cause thundering herds. Exponential backoff plus jitter fixes both.
  • Always bound retries with a max attempt count and a total deadline, and only retry transient errors.
  • Any retried operation must be idempotent, or a lost response turns one logical action into two real ones.
  • Pair backoff with a circuit breaker so you stop retrying entirely once a dependency is clearly down.
RELATED CONCEPTS
LESSONS THAT TEACH THIS
PRACTICE THIS IN REAL QUESTIONS