Part 7 · Reliability & Resilience
Timeouts, Retries & Backoff with Jitter
One slow dependency with no timeout, plus clients that retry instantly, can take down an entire site. How to set timeouts from real latency, propagate deadlines, retry only what's safe, avoid retry amplification, and why backoff needs jitter.
On this page
The Problem
The recommendations service slows down. It does not fail. It just takes 30 seconds to answer instead of 50 ms. Here is what happens next:
- The product-page service calls it without a timeout (a limit on how long to wait), so each request waits 30 seconds.
- All of the product service's threads fill up with waiting requests (post 4). A thread is one worker that handles one request at a time. Little's Law (post 8) says that slow requests need many more workers.
- The product service stops answering, and the homepage and checkout break too, because they call the product service.
- Clients see errors and retry (send the request again), which doubles or triples the load.
- The recommendations service is already struggling. It gets even more traffic and stops working completely.
One slow dependency (a service you call) took down the whole site. This is a cascading failure: one failure causes the next, like falling dominoes. The two most common causes are missing timeouts and naive retries (retries with no limits). Getting these two right is one of the most valuable reliability skills.
- t = 0recommendations slows from 50 ms to 30 s — it does not fail, it just hangs
- t + 10 sproduct service calls it with no timeout; its threads fill with waiting requests
- t + 30 sproduct service stops answering → homepage and checkout break too
- t + 40 sclients see errors and retry, doubling or tripling the load
- t + 60 srecommendations, now flooded, falls over completely
The Core Idea
Think about calling a customer support line.
- A timeout is a limit on how long you wait. It is like deciding "if nobody answers in 2 minutes, I will hang up". Without that rule, you could sit on hold forever while everything else in your day waits.
- A retry is calling again after you hang up. This makes sense if the problem was short.
- Backoff means waiting longer between each attempt: 1 minute, then 5, then 15. If the line is overloaded, calling back at once only makes the queue longer.
- Jitter means adding a bit of randomness to when you call back. Say the line went down at 10:00 and everyone calls back at exactly 10:05. It crashes again. If people call back at random times between 10:03 and 10:10, the line copes.
How It Works
Part 1: Timeouts
Every network call needs a timeout. Many libraries and HTTP clients have no timeout, or a very long one, by default. Always set timeouts yourself.
Types of timeouts:
- Connect timeout: how long to wait to open the connection. It is usually short (like 100 ms–1 s), because a healthy server accepts quickly.
- Read (response) timeout: how long to wait for data after the connection is open. In some tools, such as Python requests, this is the wait between pieces of data, not the whole download.
- Total (request) timeout: the overall limit for the whole call, including retries if you count them together.
- Idle timeouts close keep-alive connections (connections kept open for reuse) that nobody uses. Pool acquire timeouts limit how long to wait for a free connection from a pool (a ready-made set of connections that many requests share).
// Node.js fetch with a timeout using AbortControllerconst res = await fetch(url, { signal: AbortSignal.timeout(300) }); // 300 ms total# Python requests: (connect timeout, read timeout)requests.get(url, timeout=(0.5, 2.0))Choosing timeout values
Use measured latency, not guesses:
- Look at the dependency's p99 or p99.9 latency (post 8). Latency is the time a call takes. p99 is the time that 99 out of 100 calls beat. If p99 is 120 ms, a timeout of about 300–500 ms leaves room for normal slow calls and still cuts off calls that are really stuck.
- Too short: you time out on requests that are slow but normal. This creates extra retries (false failures).
- Too long: stuck calls hold resources and cause cascading failures.
Also think about how long the user will wait. If the whole page must load in 1 second, one dependency cannot be allowed 5 seconds.
Deadline propagation
Deadline propagation means passing the remaining time along the chain. In a chain of services, each step (hop) should know how much time is left:
- 1User request1,000 ms budget
- 2API gateway950 ms left
- 3Product servicefans out in parallel
- 4Pricinggiven 300 ms
- 5Reviewsgiven 300 ms
- 6Reviews DB~250 ms left — if the remaining budget is too small, fail fast or fall back
If the product service has only 200 ms left, it should not start a call that normally takes 400 ms. It should fail fast (give an error at once) or use a fallback (a simpler backup answer). gRPC has deadlines built in (post 31). In most languages you pass the incoming deadline on to the next call. With plain HTTP, you can send a deadline in a header.
Stop work when the caller has given up. If the user closed the page, or the service that called you (the upstream) has timed out, then going on with the work is a waste. Use cancellation (a way to tell running work to stop) where your framework supports it.
Part 2: Retries
A retry sends the same request again after a failure. Retries turn temporary failures into successes. But if you use them carelessly, they turn small problems into outages.
When to retry:
- ✅ Transient errors: timeouts, connection resets,
503 Service Unavailable,429 Too Many Requests(after theRetry-Afterdelay, a header in which the server says how long to wait),502/504from a proxy, and database deadlocks (two transactions that wait for each other). - ❌ Permanent errors:
400,401,403,404,422, validation failures. Retrying will not help.
Only retry safe operations:
- Idempotent operations are safe to retry. Idempotent means that doing it many times has the same result as doing it once. GET, PUT and DELETE are idempotent.
- Non-idempotent operations (POST, "charge card") are safe to retry only with idempotency keys (post 34). An idempotency key is a unique label that lets the server spot a repeated request.
Retry amplification
Here is the hidden danger, called retry amplification: retries at many layers multiply. Imagine each layer makes 3 attempts (1 try + 2 retries):
With 4 attempts per layer across 5 layers, that's 4⁵ = 1,024 calls to the bottom service for one user action. When the bottom service is already struggling, this multiplication finishes it off.
Rules to prevent amplification:
- Retry at one layer only, usually the one closest to the failure or to the edge. Do not retry at every layer.
- Cap attempts, which means set a maximum. Usually 2–3 in total.
- Use retry budgets. A retry budget is a limit on how many retries you allow. For example, retries may add at most 10% extra load. If more than 10% of recent requests are already retries, stop retrying and fail fast. (Envoy, gRPC retry throttling and some libraries support this.)
- Honour
Retry-Afterfrom servers. This means: wait at least as long as the server asks. - Combine with circuit breakers (post 42). A circuit breaker stops calls to a dependency that is clearly down, like a fuse that cuts the power.
Part 3: Exponential backoff
Exponential backoff means that the wait doubles after each failure. Do not retry at once. Wait longer after each failure:
wait = base × 2^(attempt − 1) (attempt counts from 1; capped at some maximum)base = 100 ms: attempt 1 → 100 ms attempt 2 → 200 ms attempt 3 → 400 ms attempt 4 → 800 ms attempt 5 → 1.6 s … up to a cap (e.g., 10–30 s for background work)This gives the struggling service time to recover, and it lowers the load while the service recovers.
Part 4: Jitter
Backoff alone has a flaw. Say 10,000 clients all fail at the same moment (for example, the database had a short problem). They all retry at exactly 100 ms, then 200 ms, then 400 ms. They stay in step and arrive in waves, and every wave hits the server at once. This is the thundering herd (a huge crowd of clients that all act at the same moment).
Randomness breaks the lock-step. The same number of retries arrives as a gentle, even trickle.
Jitter adds random time to the wait, so clients no longer act together. The most common forms:
(Here "attempt" counts from 0, so the first retry waits at most the base time.)Full jitter (recommended default): wait = random(0, min(cap, base × 2^attempt))Equal jitter: temp = min(cap, base × 2^attempt) wait = temp/2 + random(0, temp/2)Decorrelated jitter: wait = min(cap, random(base, previous_wait × 3))AWS tested these variants and found that full jitter and decorrelated jitter spread the load much better than plain exponential backoff. They also finish the work with far fewer total calls.
async function withRetry(fn, { attempts = 3, base = 100, cap = 2000 } = {}) { for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (err) { if (!isTransient(err) || i === attempts - 1) throw err; const delay = Math.random() * Math.min(cap, base * 2 ** i); // full jitter await new Promise(r => setTimeout(r, delay)); } }}Jitter is not only for retries. Use it anywhere many clients act on a schedule:
- cron jobs (jobs that run on a time schedule) across thousands of servers,
- cache TTLs (the time after which a cached item expires; many items that expire together cause the cache avalanche in post 16),
- client reconnects after a WebSocket server restarts (a WebSocket is a long-lived connection, post 33),
- mobile apps that sync "every hour".
Putting it together: a checklist for every network call
- 1Connect timeout and total timeoutfrom the dependency's p99 and the user's budget
- 2Deadline passed downstreamand work cancelled when the caller gives up
- 3Retry only transient errorstimeouts, resets, 503, 429 after Retry-After
- 4Only idempotent operationsor with idempotency keys
- 52–3 attempts, at one layernever at every layer
- 6Exponential backoff with full jittercapped
- 7Retry budget or circuit breakerfor when it is really down
- 8Metricstimeouts, retries and success-after-retry rates
Trade-offs
- Short timeouts: protect resources and fail fast, but risk cutting off legitimate slow requests.
- Long timeouts: fewer false failures, but a risk of resource exhaustion and cascades.
- More retries: better success during short problems, but more load during real outages, and a higher tail latency (the time of the slowest requests).
- Fewer retries: safer under stress, but users see more errors during short problems.
- Backoff and jitter: smoother recovery, but a single request may wait longer before it succeeds.
- Retrying at the edge vs in the middle: retrying close to the client gives the best view of the user's time budget. Retrying close to the dependency is faster and cheaper per attempt. Pick one layer.
In the Real World
AWS's "Timeouts, retries, and backoff with jitter." Amazon's Builders' Library article describes how Amazon services set timeouts from measured latency, limit retries (for example, retry at one layer only), and use jitter. An earlier AWS Architecture Blog post, "Exponential Backoff and Jitter", compared jitter strategies with simulations. The AWS SDKs now use exponential backoff with jitter.
Google SRE's cascading failures. Google's SRE book (SRE means site reliability engineering) describes how overload, missing deadlines and retries together cause cascading failures. It recommends deadline propagation, limited retries and retry budgets. Similar ideas are built into gRPC.
Mobile apps reconnecting. When a popular app's backend restarts, millions of clients may reconnect at once and knock it down again. Messaging and social apps use random reconnect delays (jitter), so the reconnections are spread over time.
Payment timeouts. Payment systems are the classic case where a timeout does not mean failure: the charge may have worked. This is why payment flows combine timeouts with idempotency keys and status checks ("look up the payment by its reference") instead of retrying the charge without thinking (post 34).
Interview Questions
Q1How do you choose a timeout value?
Use measured latency and the caller's time budget. Set it a bit above the dependency's p99 or p99.9 (say 300–500 ms if p99 is 120 ms), and never more than the time left in the whole request. Set separate connect and total timeouts, and never rely on library defaults.
Q2What is retry amplification, and how do you avoid it?
When every layer retries, the attempts multiply. Three attempts at four layers is 81 calls at the bottom for one user action, and they arrive exactly when that service is weakest. To avoid it: retry at one layer only, cap attempts at 2–3, use retry budgets, honour Retry-After, and add circuit breakers.
Q3Why add jitter to exponential backoff?
Without it, clients that failed together retry together in waves (a thundering herd), and the waves knock the recovering service down again. Make the wait random. Full jitter is random(0, base × 2ⁿ). It spreads the retries out and finishes the work with fewer total calls.
Q4Which errors should you retry?
Transient ones: timeouts, connection resets, 502, 503 and 504, 429 (after Retry-After), deadlocks. Do not retry 400, 401, 403, 404 or 422, because retrying cannot fix a bad request. Retry non-idempotent calls only with an idempotency key.
Q5What is deadline propagation?
You pass the remaining time budget along the call chain. So each service knows how long it has, skips work that cannot finish in time, and cancels calls in progress once the original caller has given up. gRPC has this built in. With HTTP, use a deadline header.
Q6A payment call timed out. Should you retry it?
Not blindly. A timeout does not mean it failed, because the charge may have gone through. Retry only with the same idempotency key, or first look up the payment's status by its reference.
Key Takeaways
- Every network call needs a timeout: connect, request and total. Base the values on measured p99 and the user's latency budget.
- Propagate deadlines through service chains, and stop work when the caller has given up.
- Retry only transient errors, only idempotent operations (or with idempotency keys), with 2–3 attempts max, at one layer.
- Beware retry amplification (attempts multiply across layers). Use retry budgets and circuit breakers.
- Use exponential backoff with full jitter, and add jitter anywhere many clients act on a schedule (cron, TTLs, reconnects).
Further Reading
- The AWS Builders' Library article "Timeouts, retries, and backoff with jitter"
- The AWS Architecture Blog post "Exponential Backoff and Jitter" by Marc Brooker
- Site Reliability Engineering by Google (chapter "Addressing Cascading Failures")
- Azure Architecture Center pattern "Retry"
- The gRPC documentation on deadlines
- Release It! (2nd edition) by Michael Nygard (the chapter on stability patterns: timeouts)
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series