← Computer Networks
Traffic Management & Resilience

Timeouts, Retries & Exponential Backoff

Watch: "Retry Strategies: Exponential Backoff & Jitter Explained" — ByteByteGo

Retries sound like a free win for reliability, but done naively they're one of the most common causes of a small outage becoming a full cascading failure. Picture Service A calling Service B: if B is slow and A has no timeout, A waits forever — eventually A exhausts its own threads/connections just holding open calls to a dependency that was never going to answer. This is a favorite interview probe because the "obvious" answer — just retry on failure — is usually wrong without more care, and interviewers are listening for the words "jitter" and "idempotent."

What Is Timeout, Retry & Backoff Handling?

These three ideas work together to keep one slow or failing service from taking down everything that depends on it:

Where This Is Used

A Day-to-Day Example

Imagine calling a busy restaurant to make a reservation. If no one picks up, you don't stay on the line forever — you hang up after a reasonable wait (a timeout). You try again a bit later rather than giving up entirely (a retry). And if the line is busy over and over, you naturally start waiting longer between attempts instead of redialing every ten seconds (backoff) — because redialing instantly, along with everyone else who wants a table that night, just keeps the phone line jammed.

Deep Dive: Why a Missing Timeout Kills the Caller, Not Just the Callee

Thread pool with and without a timeoutno timeout: every thread waits on B foreverT1T2T3T4T5T6T7T8→ pool full: A can't serve ANY request, even ones not needing Btimeout = 2 s: stuck calls give up, threads come backT1T2T3T4T5T6T7T8→ only a couple of threads are ever waiting; A stays healthy
When B hangs and A has no timeout, each request that touches B holds one of A's threads indefinitely until none are left, and A goes down too. A timeout caps how long a thread can be held, so the slow dependency can't drain the whole pool.
stuck foreverwaiting ≤ 2 sfree
Show text version
Service A -> Service B (B is slow/hanging, A has NO timeout)

  A's thread pool: [T1: waiting on B] [T2: waiting on B] [T3: waiting on B] ...
  Every incoming request to A that needs B ties up another thread,
  indefinitely. Eventually A's thread pool is fully saturated waiting
  on a dependency that will never respond -- A can no longer serve
  ANY request, including ones that don't even need B.
  This is how one slow dependency cascades into a total outage of
  a caller that was otherwise perfectly healthy.

  Fix: bound the wait explicitly.
  Service A -> Service B, timeout=2s
  [T1: waiting, aborts at 2s] -- thread freed, A stays healthy

Deep Dive: Why Backoff Needs Jitter, Not Just Exponential Growth

Exponential backoff alone (1s, 2s, 4s, 8s...) is not enough. Imagine 100,000 clients all get a failed response from the same backend at roughly the same moment (a brief blip). With pure exponential backoff and no randomness, all 100,000 clients compute the exact same delay and retry at the exact same moment — hitting the backend with the full 100,000-request spike again, right as it may be starting to recover. This is the "thundering herd" or retry-storm problem.

Retry spikes without jitter versus spread retriesno jitter: everyone retries at the same momentfull jitter: retries spread over each windowt=0st=0st=2st=2st=6st=6s
If 100,000 clients fail together and all wait exactly 2 s, then 4 s more, they hit the recovering server in synchronized spikes. Adding a random delay spreads the same retries into a steady trickle the server can absorb.
synchronized spikesjittered retries
Show text version
Without jitter — synchronized retry storm:
  t=0:  100,000 clients fail simultaneously
  t=2s: ALL 100,000 retry at once (same computed delay) -> spike #2
  t=6s: ALL 100,000 retry at once again -> spike #3
  Backend never gets a chance to actually recover between spikes.

With full jitter — retries spread across time:
  t=0:     100,000 clients fail
  t=0-2s:  retries spread randomly across this window (not synchronized)
  t=2-6s:  next round of retries, again spread across the window
  Backend sees a smoothed, absorbable trickle instead of a spike.

"Full jitter" — picking a random delay uniformly between 0 and the computed exponential backoff value for that attempt — is the commonly recommended approach precisely because it decorrelates clients that failed together, converting a synchronized spike into a spread-out trickle the recovering backend can actually absorb.

Key Pointers

How It Works

  1. The caller sends a request with a bounded timeout (e.g. 2 seconds).
  2. If the call times out or fails with a retryable error (5xx, connection reset), the caller checks whether the operation is safe to retry (is it idempotent?).
  3. The caller computes a backoff delay that grows exponentially from a base value, and adds random jitter to it.
  4. The caller waits the jittered delay, then retries — up to a maximum attempt count or total time budget.
  5. If retries are exhausted, the caller fails fast and surfaces the error, ideally to a circuit breaker or fallback path, rather than retrying indefinitely.

Try It Yourself

# Force a client-side timeout and observe the failure mode
curl --max-time 2 https://example.com/slow-endpoint

# Simulate a hanging server locally to see what "no timeout" costs
# you -- watch the client thread/connection sit blocked
python3 -m http.server 8000 &  # then point a slow-responding route at it

# Retry a flaky call manually with backoff+jitter in shell, to feel
# the difference between flat retry and jittered exponential backoff
for i in 1 2 3 4; do
  curl -s -o /dev/null -w "attempt $i: %{http_code}\n" https://example.com/api
  delay=$(( (2**i) ))
  jitter=$(( RANDOM % delay ))
  sleep $jitter
done

# Load-test a flaky/rate-limited endpoint and watch how many clients
# would collide on the same retry delay without jitter
hey -z 10s -c 50 https://example.com/api

# Inspect whether a real API tells clients how long to back off
curl -sI https://api.example.com/v1/resource | grep -i retry-after

Interview Q&A

Q: Service A calls Service B and B is slow. What do you do?

A: First, set an explicit timeout on the call from A to B — without one, A's own threads/connections get tied up indefinitely waiting on a response that may never come, and that alone can take A down even if A had no other problems. Once there's a bounded timeout, I'd add retries with exponential backoff and jitter for transient failures, but only if the call is idempotent — otherwise a retry risks duplicating a side effect. Finally, I'd wrap repeated calls to B in a circuit breaker so that once B is clearly unhealthy, A stops calling it altogether instead of continuing to retry into a dependency that's down.

Q: Why does jitter matter — isn't exponential backoff enough on its own?

A: No — pure exponential backoff without randomness means every client that failed at the same moment computes the identical delay and retries in lockstep, recreating the exact same traffic spike against the backend on every retry round. Jitter (randomizing the delay, typically uniformly between 0 and the computed backoff) decorrelates those clients so retries spread out over time instead of arriving as synchronized waves — this is what actually gives a recovering backend room to breathe.

Q: Are retries always safe to add?

A: No — only for idempotent operations. A GET is safe to retry by definition. A POST that creates a resource or charges a payment is not safe to retry blindly, because if the original request actually succeeded but the response was lost in transit, a retry would duplicate the side effect (double order, double charge). The fix is either restricting retries to genuinely idempotent operations, or making the operation idempotent artificially via an idempotency key the server deduplicates on.

System Design Tie-in

This is foundational to any "design a resilient/reliable microservices system" question — interviewers expect every inter-service call in your design to have an explicit timeout, and expect you to proactively mention retry-with-backoff-and-jitter rather than needing to be prompted. It's also the natural setup for the circuit breaker follow-up: once you've described timeouts and retries, the next question is almost always "what stops A from retrying forever into a B that's clearly down" — which is exactly what a circuit breaker solves. In payment or order-processing system designs specifically, expect a direct question about idempotency keys tied to retry safety.

Trade-offs

Retries improve resilience against transient blips, but done naively they amplify load exactly when a downstream dependency is already failing. Exponential backoff with jitter mitigates this but doesn't eliminate it, and it adds latency on the failing path that the caller must budget for. Interview gotcha: timeouts and retries must be tuned together — a retry policy paired with an overly long timeout still risks resource exhaustion, and retries without a genuine idempotency guarantee are unsafe no matter how good the backoff strategy is.