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."
These three ideas work together to keep one slow or failing service from taking down everything that depends on it:
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.
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 healthyExponential 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.
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.
# 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
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.
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.
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.