Module A-7·28 min read

Failing fast on purpose, isolating pools so one dependency cannot eat the whole thread budget, and shedding features instead of dropping requests.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module A-7 — Circuit Breakers, Bulkheads, and Graceful Degradation

What this module covers: What a circuit breaker adds that a timeout cannot — stopping calls entirely once it is clear they will fail, which both saves your resources and removes the load keeping the dependency down. The state machine in enough detail to configure it correctly: why a rolling failure rate with a minimum-volume guard beats a consecutive-failure count, why a 4xx must not count as a failure, and why a slow-call-rate threshold matters as much as an error threshold. Bulkheads as resource isolation, and the specific Node.js form of them, since there are no thread pools to partition. Then the part that is design work rather than configuration: graceful degradation — classifying every dependency as critical or optional, choosing a fallback that is genuinely cheaper and does not depend on the thing that failed, and deciding fail-open versus fail-closed explicitly. Finally the production insight that ties it together: degradation removes failure from your error rate, so a system can serve fallbacks permanently while every dashboard stays green.


A Timeout Bounds Each Call; a Breaker Stops Making Them

Module A-6 gave you a bound per call. Now consider a dependency that is genuinely down, with a 2-second timeout and 500 requests per second arriving:

text

The timeout did its job — each request failed in bounded time — and the aggregate is still an outage. You are spending your entire concurrency budget discovering, five hundred times a second, something you already knew.

A circuit breaker is the observation that after enough failures you can stop asking. It converts a 2-second failing call into a sub-millisecond local rejection, which does three things at once:

  1. Frees your resources. Nothing is held for two seconds.
  2. Removes load from the dependency, giving it room to recover — the exit from Module A-6's metastable state.
  3. Fails fast enough for a fallback to be viable. A 2-second wait before serving a cached value is a bad experience; a 1-millisecond rejection then a cache read is a good one.

Point 2 is easy to undervalue and is often the decisive one. A dependency at 100% error rate does not recover while receiving its full traffic plus retries; a breaker is how the traffic stops without a human intervening.

Analogy: the electrical circuit breaker the pattern is named for, and it is worth being precise about why it exists. It does not protect the appliance that shorted — that appliance is already broken. It protects the rest of the house from the fault, by isolating a circuit so the wiring does not overheat and the other rooms keep their lights on. Then it needs a human, or a delay, before reclosing — because closing it immediately onto an unfixed short achieves nothing except another trip.


The State Machine, and the Four Ways It Is Misconfigured

text

Simple enough to write in an afternoon, and four configuration decisions determine whether it helps or hurts.

1. Use a failure rate over a rolling window, with a minimum-volume guard. The obvious implementation counts consecutive failures, and it fails in both directions: on a low-traffic endpoint, five consecutive failures might span twenty minutes and open a breaker for a problem long since resolved; and on a partially-degraded dependency where every other call succeeds, a consecutive counter never reaches five while half your requests fail.

ts

The minimum-volume guard is what stops the most annoying false positive: the first request after a quiet period fails transiently, giving a 100% failure rate over a sample of one, and the breaker opens for a dependency that is fine.

2. A 4xx is not a failure. This is the misconfiguration that causes the most confusion. A 400, 404 or 422 means the dependency is healthy and rejected your request — validation failed, the resource does not exist, the input was malformed. Counting those as breaker failures means a client bug, a scanner probing for URLs, or one badly-formed integration can open your breaker and take down a working dependency for everyone.

Count as failures: timeouts, connection errors, 5xx, and 429 only if you choose to treat rate limiting as a signal to back off entirely (usually better handled by honouring Retry-After, Module P-18).

3. Threshold on slow calls too, not just errors. A dependency returning 100% success at 8 seconds per call is as damaging as one that is down — worse, in a sense, because nothing looks like an error. A slow-call-rate threshold ("if more than 60% of calls exceed 1 second, open") catches the degradation mode that an error-rate breaker misses entirely, and it is the check most implementations omit.

4. Half-open must admit a trickle, not a flood. A cooldown that expires and lets all 500 requests/second through at once re-hammers a recovering dependency and immediately reopens. Admit one to three concurrent probes, require a small number of successes before closing, and consider ramping — half-open at 5% of traffic, then 25%, then closed. The reopening path should also back off exponentially: a dependency that has failed its probe four times should not be probed every 10 seconds forever.

Granularity: per dependency, and per operation where they differ. One breaker for "the payments service" is usually right; one breaker for "the whole downstream world" is useless, because an unrelated failure blocks working functionality. If one endpoint of a dependency is expensive and flaky while the rest are fine, give that endpoint its own breaker.

And distinguish this from outlier ejection (Module A-5), which operates at instance level: the load balancer or mesh removes the one bad replica from rotation, which is the right response to one sick instance. A circuit breaker responds to the dependency as a whole being unusable. You want both, and they are not substitutes — outlier ejection routes around a partial failure, a breaker gives up on a total one.


Bulkheads: Stop One Dependency Consuming Everything

A ship's hull is divided into watertight compartments so a breach floods one and not the vessel. The system equivalent is: partition the resource that requests compete for, so one dependency's slowness cannot consume all of it.

Concretely, the resource being protected is different in different runtimes, and naming it matters:

RuntimeThe scarce shared resourceBulkhead form
JVM / thread-per-requestworker threadsa thread pool per dependency
Node.jsevent-loop time, sockets, and pending promisesa concurrency semaphore per dependency
Anydatabase connectionsa separate pool per workload
Anyworker capacityseparate queues and worker pools (Module P-19)
Anythe whole processa separate deployment (Module A-4)

The failure a bulkhead prevents:

text

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.