Skip to content

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.

Circuit breaker states.

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:

  1. The client generates a unique key per logical operation and sends it with the request (and every retry).
  2. The server stores the key with the result when it first processes the request.
  3. 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

Azure2

Google Cloud2

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.

  1. 01A service is overloaded and slow. Every client retries immediately, three times. What happens?
  2. 02A payment request times out. You don't know whether the charge went through. How can you retry safely?
  3. 03What does an open circuit breaker do?
  4. 04Why add jitter to backoff delays?
  5. 05What's a fallback?
  6. 06Why is a missing timeout dangerous?
  7. 07What does the bulkhead pattern do?

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.