SysDesignPrep.com
Interactive concepts

Exponential backoff and jitter: an interactive retry simulator

Exponential backoff is a retry strategy: after each failed attempt a client waits longer before it tries again, usually doubling the wait up to a cap. Jitter makes each wait random, so that clients that failed together do not all retry together. Backoff gives a struggling server room to recover; jitter stops the retries from arriving as a wave.

In the playground, a crowd of clients calls a server at the same moment, more than it can serve. Each strategy gets the same clients and the same server, and each lane shows the calls the server sees every 100 ms: green it served, red it turned away. Compare how many calls each strategy makes and when its last client is done.

Serves per 100 msDown for

100 clients call at once and the server can serve 10 per 100 ms. Fixed delay makes 1,647 calls to serve 100. Exponential backoff without jitter keeps the clients in lockstep, so they retry in waves, making 631 calls, and the last is done at 42.7 s. Decorrelated jitter is done first, at 2.2 s, with the fewest calls (340).

Fixed delaybase
calls1,647all done2.5 s
Exponentialmin(cap, base × 2ⁿ)
keeps going to 42.7 s →
calls631all done42.7 s
Full jitterrandom(0, min(cap, base × 2ⁿ))
calls494all done3.7 s
Equal jitterv / 2 + random(0, v / 2)
calls458all done2.7 s
Decorrelated jittermin(cap, random(base, previous × 3))fastestfewest calls
calls340all done2.2 s
0 s5 s
servedrejected, will retrywhat the server can serve per 100 msBars are calls per 100 ms; a cut-off top means more than 50. Base delay 100 ms, cap 10 s.

How exponential backoff works

  1. Try the call. If it succeeds, or fails with an error that will not change (a 400 or a 404), stop.
  2. Wait min(cap, base × 2attempt): with a 100 ms base, 100 ms after the first failure, then 200 ms, 400 ms, 800 ms, until the cap.
  3. Add jitter: pick the actual wait at random, for example anywhere between zero and that value.
  4. Give up after a few attempts, or when the caller’s deadline would pass, and return the error.

The doubling means a client that keeps failing backs off fast: five failures in, it is waiting over a second and a half between tries instead of hammering the server ten times a second.

Why backoff needs jitter

Clients that fail at the same moment and back off by the same rule retry at the same moment. In the first preset, 100 clients arrive together at a server that can serve 10 every 100 ms. With plain exponential backoff the 90 it turns away all come back together 100 ms later, then at 300 ms, 700 ms and 1.5 s, each time as a single spike of which the server serves at most 10. It makes 631 calls, and because the waits keep doubling, the last client is done only after 42.7 seconds. With decorrelated jitter the same clients make 340 calls and are all done in 2.2 seconds, because their retries are spread out and arrive at a rate the server can serve.

The same thing happens at larger scale whenever many clients fail together: a deploy, a dropped connection pool, a cache that expires for everyone at once, or a service coming back after an outage. This is the thundering herd, and jitter is the cheapest fix for it.

Retry storms: when retries keep a server down

Turning a call away is cheaper than serving it, but not free: the server still accepts the connection, parses the request and sends an error. The playground assumes a rejection costs a tenth of a served call. In the Half the capacity preset, 100 clients retry every 100 ms with a fixed delay at a server that can serve 5 per 100 ms: the retries alone cost more than it can do, so after the first 5 it never serves anyone again, and after two minutes 95 clients are still retrying. Nothing is broken any more except the retries themselves.

This is a retry storm, and the state it leaves behind is called a metastable failure: the overload sustains itself after its cause is gone, and the system only recovers when something cuts the load, such as clients giving up or an operator shedding traffic. Backoff, jitter, attempt limits and retry budgets are how clients avoid causing one.

Full, equal and decorrelated jitter

StrategyWait before retry nShortest waitNotes
Fixed delaybasebaseSimple, but a crowd of clients keeps retrying at the same rate however long the failure lasts
Exponentialmin(cap, base × 2ⁿ)the full valueBacks off, but clients stay in lockstep and retry in waves
Full jitterrandom(0, min(cap, base × 2ⁿ))zeroThe most spread out; the common default, used by the AWS SDKs
Equal jitterv / 2 + random(0, v / 2)half the valueGuarantees some wait, so it never retries straight away
Decorrelated jittermin(cap, random(base, previous × 3))baseEach wait builds on the last one rather than on the attempt number

With heavy contention, all three jitter strategies make far fewer calls than either strategy without jitter, and finish far sooner than plain exponential backoff. Fixed delay can finish quickly when the server keeps up, as after the 3 second outage, but only by making six or seven times the calls, and with a little less capacity it never finishes at all. Which of the three jitter strategies does best moves around as you change the clients, the capacity or the outage, so the choice between them matters much less than having jitter at all. Marc Brooker’s 2015 AWS article on backoff and jitter, which compared these same strategies, reached the same conclusion and recommended full jitter as a default.

Retrying safely

  • Retry only what can succeed. Timeouts, connection errors, 429 and 503 can; 400, 401, 403 and 404 will fail the same way again.
  • Make the request safe to repeat. A timed-out payment may have gone through. Retry writes only when they are idempotent or carry an idempotency key.
  • Honour Retry-After. When a 429 or 503 says how long to wait, wait at least that long.
  • Bound it by a deadline. Stop retrying when the caller has given up; a retry nobody is waiting for is pure load.
  • Retry at one layer. If three layers each make up to four attempts, one user request can become 4 × 4 × 4 = 64 calls at the bottom.
  • Use a retry budget. Cap retries at a share of normal traffic, say 10%, so that when everything is failing, retries stop adding load. Finagle has retry budgets, the AWS SDKs a retry quota and gRPC retry throttling.
  • Add a circuit breaker. When a dependency keeps failing, a circuit breaker stops the calls altogether instead of retrying each one.

Exponential backoff in code

import random, time

RETRYABLE = {429, 500, 502, 503, 504}

def call_with_retries(send, attempts=4, base=0.1, cap=5.0, deadline=10.0):
    start = time.monotonic()
    for attempt in range(attempts):
        response = send()                         # send() has its own timeout
        if response.status not in RETRYABLE:
            return response                       # success, or an error retrying won't fix
        if attempt == attempts - 1:
            break
        wait = random.uniform(0, min(cap, base * 2 ** attempt))  # full jitter
        wait = max(wait, float(response.headers.get("Retry-After", 0)))
        if time.monotonic() - start + wait > deadline:
            break                                 # the caller has stopped waiting
        time.sleep(wait)
    return response

Most HTTP and cloud SDK clients have this built in; check what yours does before adding another layer of retries on top of it.

In a system design interview

Retries come up wherever one service calls another: a payment provider, a push notification service, a webhook endpoint. Say the policy in one sentence: “Workers retry timeouts and 5xx up to 5 times with exponential backoff and full jitter, 200 ms base and 30 s cap, every send carries an idempotency key, and after the last attempt the message goes to a dead letter queue.” Then expect the follow-ups: what happens when the provider is down for an hour (a circuit breaker and a queue that holds the work), how you avoid sending twice, and what stops a retry storm when it comes back.

Read the resilience guide for timeouts, retries and bulkheads, or watch a strong candidate design a notification system, where retries to push and email providers are part of the design.

Questions people ask

What is exponential backoff?

A retry strategy in which a client waits longer after each failed attempt, usually doubling the wait up to a cap: 100 ms, 200 ms, 400 ms and so on. It gives an overloaded or recovering service room to catch up instead of hitting it again at the same rate, and it is usually combined with jitter and a limit on the number of attempts.

What is jitter in exponential backoff?

Randomness added to each wait so that clients that failed at the same moment do not retry at the same moment. Without it, clients stay in lockstep: they fail together, wait the same 100, 200, 400 ms and retry together, so every retry is another spike. With jitter their retries spread across the wait, and the server sees a steady trickle it can serve.

What is the difference between full, equal and decorrelated jitter?

Full jitter waits a random time between zero and the exponential backoff value. Equal jitter waits half the backoff value plus a random amount up to the other half, so it never waits less than half. Decorrelated jitter waits a random time between the base delay and three times its previous wait, capped. All three spread retries out; which is best depends on the load, and having jitter matters far more than which kind.

What base delay, cap and number of retries should I use?

Common starting points are a base of 50 to 200 ms, a cap of a few seconds to 30 seconds, and 3 to 5 attempts for a user-facing call, with the whole retry loop bounded by the caller’s own deadline. Background jobs can retry for longer, with longer caps. If the server sends a Retry-After header, wait at least that long.

Which errors should a client retry?

Retry failures that may succeed next time: timeouts, connection errors, 429 Too Many Requests, 503 Service Unavailable and most other 5xx errors. Do not retry 4xx errors such as 400, 401, 403 or 404, which will fail the same way again. Only retry a request that changes data if it is idempotent or carries an idempotency key, so a retry after a lost response does not do the work twice.

What is a retry storm?

A flood of retries that keeps a service overloaded after the original cause has gone. Every rejected call comes back, often several times, and each one costs the server work, so it spends its capacity turning clients away and cannot catch up. Backoff, jitter, a cap on attempts, retry budgets and circuit breakers all exist to stop it.

How do retries with backoff and circuit breakers work together?

Backoff with jitter handles individual failures: wait, then try again, a few times. A circuit breaker handles a dependency that keeps failing: it stops calls altogether for a while. Put the breaker outside the retry loop so that a dead service stops being called, and retry at one layer of the stack only, since three layers each making four attempts turn one request into 64.