Module 06 // Reliability and trade-offsLesson 23 / 3418 min read
Failure handling: retries, backoff, circuit breakers, idempotency
In distributed systems, calls fail all the time. Timeouts, retries with backoff, circuit breakers and idempotent operations let you recover from those failures without making things worse.
Think of it like this
Analogy
You call a friend and nobody answers. You don't hang on the line forever (a timeout). You try again, but not every second; you wait a minute, then five, then an hour (exponential backoff), and you don't call at exactly the same moment as everyone else trying to reach them (jitter). If they've been unreachable all day, you stop calling and text later (circuit breaker). And if you're asking them to send you $20, you say “just once, please, even if I ask twice” (idempotency).
The problem it solves
Every network call can fail: the server crashed, a packet was lost, a dependency is slow, a deploy is rolling out. At scale, something is always failing somewhere. Naive handling makes it worse:
- No timeout: threads wait forever and the caller runs out of capacity too.
- Instant unlimited retries: a struggling service gets hit even harder, and a small blip becomes an outage.
- Retrying non-idempotent operations: customers get charged twice, emails go out three times.
A real example
Example
Checkout waiting on a slow shipping service
Checkout asks an outside shipping service for delivery prices. One afternoon that service starts taking 30 seconds to answer.
Checkout had no timeout. Every request waited, the server's worker threads all filled up, and soon checkout couldn't serve anyone. One slow dependency took down the whole page.
The fix uses four patterns. A timeout of 800 ms. One retry with backoff and jitter. A circuit breaker that stops calling the service after repeated failures and checks again every 30 seconds. And a fallback: show a standard flat shipping price, so people can still buy.
How it works
1. Timeouts on every call
Always set a timeout, based on the dependency's normal latency (for example a bit above its p99). For a chain of calls, pass a deadline down so inner calls know how much time is left and don't keep working on a request the user has already given up on.
2. Retries, only for the right failures
Retry errors that are likely temporary: timeouts, connection resets, 503 Service Unavailable, 429 Too Many Requests (after the Retry-After delay). Don't retry errors that will fail again: 400 Bad Request, 401, 404, validation errors.
Limit attempts (2–3 is typical) and retry at one layer only. If every layer of a five-service chain retries three times, one failure becomes 3⁵ = 243 calls.
3. Exponential backoff with jitter
Wait longer between each attempt: 100 ms, 200 ms, 400 ms, 800 ms… That's exponential backoff. Add randomness, jitter, so clients don't move in lockstep. A common form is “full jitter”: wait a random time between 0 and the current backoff cap.
async function callWithRetry<T>(fn: () => Promise<T>, attempts = 3, baseMs = 100, capMs = 2000) {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (err) {
if (i + 1 >= attempts || !isRetryable(err)) throw err;
const backoff = Math.min(capMs, baseMs * 2 ** i);
await sleep(Math.random() * backoff); // full jitter
}
}
}4. Circuit breakers
A circuit breaker wraps calls to a dependency and tracks failures.
While open, calls fail instantly, often with a fallback (cached data, a default, “try again later”). That keeps your threads free and gives the dependency time to recover.
5. Idempotency: make retries safe
An operation is idempotent if doing it twice has the same effect as once. GET, PUT and DELETE are naturally idempotent; POST usually isn't. Make it so with an idempotency key:
- The client generates a unique key per logical operation and sends it with the request (and every retry).
- The server stores the key with the result when it first processes the request.
- A repeat with the same key returns the stored result instead of doing the work again.
Payment APIs work this way. The same idea makes queue consumers safe (lesson 17).
6. Other tools
- Bulkheads: separate thread or connection pools per dependency, so one slow dependency can't use up everything.
- Load shedding: under overload, reject low-priority work early (with 503) instead of slowing everything down.
- Fallbacks and graceful degradation: show cached or partial results.
- Dead-letter queues for async work that keeps failing.
- Chaos testing: inject failures deliberately to check that all of the above actually works.
Trade-offs
What you gain
- Turns brief failures into invisible blips instead of user-facing errors.
- Prevents one failing dependency from taking down everything that calls it.
- Idempotency makes retries, queues and at-least-once delivery safe.
What it costs you
- Retries add latency and load; done badly they cause retry storms.
- Circuit breakers and timeouts need tuning, and wrong values cause false trips or slow failure.
- Idempotency keys need storage and expiry, and careful handling of concurrent duplicates.
When not to use it
- Don't retry non-idempotent operations without an idempotency key.
- Don't retry at every layer of a call chain; pick one.
- Don't retry errors that can't succeed, like validation failures.
In the cloud
Workflows with retriesEvery major cloud sells this as a managed service, so you configure it rather than build it.
AWS2
- AWS Step Functions (opens vendor docs)
Orchestrates steps as a state machine with built-in retries, backoff and error handling.
- AWS Fault Injection Service (opens vendor docs)
Runs controlled experiments that break things on purpose to test your recovery.
Azure2
- Durable Functions (opens vendor docs)
Write long-running workflows in code; state, retries and checkpoints are handled for you.
- Azure Chaos Studio (opens vendor docs)
Injects faults like VM shutdowns and network latency to test resilience.
Google Cloud2
- Workflows (opens vendor docs)
Orchestrates service calls with retry policies and error handling.
- Cloud Tasks (opens vendor docs)
Retries failed HTTP tasks with configurable exponential backoff.
Interview questions
Try answering out loud first, then open the model answer.
How do you design a payment API so clients can retry safely?
Model answer
Require an Idempotency-Key header on POST /payments. On first receipt, insert the key with status “in progress” atomically, using a unique constraint so concurrent duplicates are rejected or wait, then process the charge and store the response with the key. Retries with the same key return the stored response; a different body with the same key is an error. Keys expire after, say, 24 hours. Downstream calls to the card processor also use idempotency keys, and the client retries with backoff and jitter on timeouts and 5xx only.
What's a retry storm and how do you prevent one?
Model answer
When a service slows down, clients time out and retry, multiplying load exactly when the service can least handle it; with retries at several layers the multiplication compounds, and the service can't recover. Prevention: exponential backoff with jitter, a small attempt limit, retrying at only one layer, retry budgets (for example retries at most 10% of requests), circuit breakers that stop calling a failing service, and server-side load shedding with 429 or 503 plus Retry-After.
Explain how a circuit breaker works.
Model answer
It wraps calls to a dependency. Closed: calls go through and failures are counted. When the failure rate in a window crosses a threshold, it opens: calls fail immediately, optionally returning a fallback, without touching the dependency. After a cool-off it goes half-open and lets a few trial calls through; if they succeed it closes, and if not it opens again. This avoids wasting time and threads on a dependency that's down and gives it room to recover.
Quiz
Pick an answer to see whether it’s right and why.
0 of 7 answered
Key terms
- Timeout
- The longest you'll wait for a response before giving up.
- Retry
- Trying a failed request again, usually after a short wait.
- Exponential backoff
- Waiting longer after each failed attempt (1 s, 2 s, 4 s…), so retries don't pile on a struggling service.
- Jitter
- Random variation added to retry delays so many clients don't retry at the same instant.
- Circuit breaker
- A switch that stops calling a failing service for a while, failing fast instead of waiting.
- Idempotency
- Doing the same operation twice has the same effect as doing it once, which makes retries safe.
Progress is saved in this browser only.