Module 08 · Lesson 01

Timeouts, retries, circuit breakers and bulkheads

Most outages are a slow dependency plus unlimited patience.

Intermediate 8 min read Updated 2026-11-02

What you will be able to do

  • Set a timeout budget across a request path
  • Apply retries with backoff and jitter without causing a storm
  • Explain what a circuit breaker protects and what it hides
  • Isolate dependencies with bulkheads and pool limits

The retry amplification table

Retries are the first thing engineers add and the most common cause of a self-inflicted outage. Each layer that retries multiplies the load on the layer below it.

Layers retryingAttempts per layerRequests reaching the dependency
133
239
3327
4381

At 81x, a dependency that was merely slow becomes unavailable, and every retry storm looks identical from the inside. The fix is to retry at one layer only — usually the outermost client — and to make every layer below it fail fast.

Only retry what is safe to retry
Retry reads and idempotent writes. Never retry a non-idempotent write without an idempotency key, unless you enjoy duplicate charges and double orders.

Timeouts are a budget

If the user waits 800 ms before abandoning the page, then the whole path — gateway, service, cache, database, third-party API — must fit inside 800 ms. Allocate it: 50 ms gateway, 20 ms auth, 600 ms service work, 100 ms slack. A dependency with a 5 s timeout inside an 800 ms budget is not protecting anything; it is allowing a thread to sit idle while the user has already left.

Connect timeout
50-200 ms
Shorter than the request timeout
Read timeout
remaining budget
Pass it down explicitly
Backoff base
50-200 ms
Exponential, capped
Jitter
full or equal
Prevents synchronized retries
for attempt in 1..3:
  remaining = deadline - now()
  if remaining < 50ms: break          # no budget left
  try call(dep, timeout=min(remaining, 300ms))
  catch transient: sleep(rand(0, base * 2^attempt))
  catch permanent: raise immediately   # never retry 400s

Circuit breakers

A breaker watches recent failures for a dependency. Past a threshold it opens and returns failures immediately without calling the dependency; after a cool-down it lets a few probes through, and closes if they succeed. The value is not the saved call — it is the thread that is not blocked for two seconds while the dependency times out.

  • Open state needs a fallback. Cached data, a default, a degraded response, or an honest error. An open breaker with no fallback is just a faster 500.
  • Per dependency, not global. One shared breaker means a failing search service disables payments.
  • Tune on error rate, not a single error. One timeout in a thousand requests should not trip it.
  • Half-open with a small probe count. Flooding a recovering service re-opens the breaker immediately.

Bulkheads

A bulkhead gives each dependency its own bounded pool and queue. Without one, a slow third-party call consumes every worker thread, and requests that never touch that dependency also fail. This is the single most common shape of a cascading failure, and the fix is a number: at most 20 concurrent calls to the recommendation service, queue of 50, reject beyond that.

Cap concurrency per dependency
Pick a limit from the dependency's capacity, not from your traffic.
Bound the queue
An unbounded queue turns latency into memory pressure and then into a crash.
Reject fast when saturated
Return a degraded response rather than accepting work you cannot finish in time.
Test with a dependency down
Chaos-test the slow path; timeouts and fallbacks that were never exercised are usually wrong.
Say the budget out loud
"The page budget is 800 ms. Each dependency gets a timeout that fits inside it, one layer retries with jitter, breakers return a cached fallback, and each dependency has its own pool so a slow one can't take down the rest."

Worth remembering

  • A timeout should be derived from the caller's remaining budget, not picked as a round number.
  • Retries multiply traffic: three attempts across four layers is 81 requests for one user action.
  • Circuit breakers convert a slow failure into a fast failure, which is what keeps threads alive.
  • Every dependency needs its own bounded pool, or one slow call starves all of them.

Checkpoint

Answer all 3 questions to finish the lesson.
1. Three layers each retry three times. How many requests can reach the dependency for one user action?
3 × 3 × 3 = 27. Retry amplification is multiplicative, which is why retries belong at one layer.
2. What is the primary value of a circuit breaker?
Blocked threads are the mechanism of cascading failure. Returning quickly preserves capacity for work that can still succeed.
3. A slow recommendation service is starving threads for checkout. Best structural fix?
Bulkheads isolate failure domains. A bigger shared pool just delays the same collapse and makes it larger.