Part 7 · Reliability & Resilience
Circuit Breaker & Bulkhead Patterns
Timeouts limit how long you wait per call, but not how often you keep calling a service you already know is dead. How circuit breakers fail fast and test for recovery, how bulkheads stop one dependency exhausting everything, and how to design honest fallbacks.
On this page
The Problem
You have added timeouts and careful retries (post 41). Now the reviews service goes completely down. Every product-page request still does this:
- calls reviews,
- waits the full 500 ms timeout (the longest time it will wait),
- maybe retries once, and waits another 500 ms,
- then gives up.
So every product page is now 1 second slower. At 5,000 requests per second, thousands of threads are stuck waiting for a service that you already know is dead. (A thread is one worker that handles one request at a time.) Those threads are shared with pricing, stock and checkout calls. Soon everything is slow.
Timeouts limit how long you wait per call. What is missing is a way to stop calling a dependency that is clearly broken. A dependency is another service that you call. You also need to make sure that one bad dependency cannot use up all your resources. That is what circuit breakers and bulkheads do.
The Core Idea
A circuit breaker is like the one in your home's electrical panel. When there is a dangerous fault (a short circuit), the breaker trips and cuts the power at once. This protects the wiring and the house. After the problem is fixed, you reset it. In software, a circuit breaker is a small piece of code that watches the calls to a dependency. When too many calls fail, it trips and stops sending calls for a while. It returns an error at once (this is called failing fast) instead of waiting. Then it carefully tests whether the dependency has recovered.
A bulkhead is like the compartments in a ship. Ships are divided into watertight compartments (bulkheads). If the hull is damaged, only one compartment fills with water, and the ship stays afloat. In software, a bulkhead gives each dependency its own separate pool of resources (threads, connections, instances). A pool is a fixed set of items that can be shared and reused. So one failing dependency can use up only its own pool, not everyone's.
How It Works
Circuit breaker: three states
- Send a few calls, then make the service sick.
1. Closed (normal). Calls go through as usual. The breaker counts successes, failures and slow calls in a rolling window. A rolling window is the most recent set of calls, such as the last 100 calls or the last 10 seconds.
2. Open (tripped). When failures cross a threshold (a limit; for example, "more than 50% failed, with at least 20 calls in the window"), the breaker opens. Now every call fails immediately, without contacting the dependency. The app returns a fallback (a simpler backup answer) or a fast error.
- The dependency gets space to breathe and recover, because no pile of requests hits it.
- Your service does not waste threads on waiting for timeouts.
3. Half-open (testing). After a cool-down period (a wait time; for example, 30 seconds), the breaker lets a few test calls through.
- If they succeed, the breaker goes back to closed.
- If they fail, it goes back to open for another cool-down.
What counts as a failure?
- Errors: exceptions, 5xx responses, connection failures.
- Timeouts.
- Slow calls. Many libraries can trip on "more than X% of calls slower than 2 seconds", because slowness is often the first sign of trouble.
- Usually not client errors like
400or404. Those mean that the request was wrong. They do not mean that the dependency is unhealthy.
Fallbacks: what to do when the circuit is open
A circuit breaker is only as good as its fallback. A fallback is what you do instead when the real call is not possible:
| Dependency down | Fallback |
|---|---|
| Recommendations | Show "popular items" from a cache, or hide the section |
| Reviews | Hide reviews, show "Reviews temporarily unavailable" |
| Profile pictures | Show default avatars |
| Exchange rates | Use the last known cached rate (with a timestamp) |
| Search | Show a simpler category browse |
| Payments | No fake fallback! Show a clear error: "Payment is temporarily unavailable, please try again" |
Never fake success for critical operations. A fallback should degrade the experience in an honest way (give a simpler result), and not hide a real failure (post 44).
// Resilience4j-style example (Java)CircuitBreaker cb = CircuitBreaker.of("reviews", CircuitBreakerConfig.custom() .failureRateThreshold(50) // open at 50% failures .slowCallRateThreshold(50) .slowCallDurationThreshold(Duration.ofMillis(800)) .minimumNumberOfCalls(20) .waitDurationInOpenState(Duration.ofSeconds(30)) // cool-down .permittedNumberOfCallsInHalfOpenState(5) .build());Supplier<List<Review>> call = CircuitBreaker.decorateSupplier(cb, () -> reviewsClient.get(productId));List<Review> reviews = Try.ofSupplier(call).getOrElse(Collections.emptyList()); // fallbackBulkheads: isolate resources
Without bulkheads, one shared pool serves every dependency. The pool is limited, so it can run out:
With bulkheads, each dependency has its own limit:
- −FULL — extra calls rejected at once
- +Goes straight to the fallback: hide reviews
- +Working normally ✅
- +Working normally ✅
Reviews can only ever use its 30 slots. When they are full, extra review calls are rejected at once (and go to the fallback), and pricing and stock keep working.
Ways to build bulkheads:
- Separate thread pools or concurrency limits (semaphores) per dependency. A semaphore is a counter that allows only a fixed number of calls at the same time.
- Separate connection pools per database or downstream service (a downstream service is one that you call).
- Separate queues and workers per job type (like the priority queues in post 38).
- Separate instances or clusters (groups of servers) for critical and non-critical traffic. For example, a dedicated set of servers for checkout, kept apart from browsing traffic.
- Per-tenant limits (a tenant is one customer or one customer account), so one noisy customer cannot use all the capacity (like the cells and shuffle sharding in post 40).
- Kubernetes resource limits per container, so one runaway service cannot eat all the CPU and memory of a node (a node is one machine in the cluster).
How the patterns work together
- Request
- Bulkheadmax 30 concurrent
- Circuit breakeropen → fail fast
- Timeout500 ms
- Retry×1 with jitter
- Reviews service
- Timeouts limit each call.
- Retries handle short problems.
- Circuit breakers stop calls to a clearly broken dependency.
- Bulkheads stop any one dependency from exhausting shared resources.
- Fallbacks keep the user experience as good as possible.
Where to put them
- In application code, with libraries: Resilience4j (Java), Polly (.NET), opossum (Node.js), pybreaker (Python) and gobreaker (Go).
- In the network layer, with a proxy or a service mesh. A proxy is a server that passes traffic on. A service mesh is a set of proxies that manage traffic between your services. Envoy, Istio and Linkerd offer circuit breaking (limits on connections and requests) and outlier detection, which removes single unhealthy hosts from load balancing for a while. You need no code changes, but it knows less about your business fallbacks.
Many systems use both: the mesh for general protection, and the app for smart fallbacks.
Pitfalls
- Breakers that hide problems. An open breaker with a quiet fallback can hide an outage for hours. Send an alert when a breaker opens.
- Wrong scope. One breaker for a whole service can trip because of one bad host. A breaker for each host may miss a problem that affects the whole service. Choose with care (outlier detection often works per host).
- Thresholds too sensitive, so the breaker trips on normal small errors. Use a minimum number of calls and rolling windows.
- Thresholds too loose, so the breaker does not trip in time to help.
- No fallback, so an open breaker only returns errors faster. That is still better than hanging, but plan real fallbacks for non-critical features.
- Bulkheads with the wrong size. If they are too small, you reject requests during normal busy times. Work out the size with Little's Law (post 8): concurrency ≈ requests per second × latency. (Latency is how long one call takes. Concurrency is how many calls run at once.)
Trade-offs
- Circuit breakers: they fail fast, protect the dependency and save your resources. But they need tuning, add states to think about, and may cause false trips (a trip when the dependency was fine).
- Bulkheads: strong isolation, so one failure stays contained, but resources are reserved per pool, so idle pools cannot lend capacity to busy ones.
- Fallbacks: a better user experience during failures, but extra code paths to build and test. Fallbacks that were never tested often fail too.
- Mesh-level vs app-level: no code changes, or behaviour that knows your business. It is often best to use both.
In the Real World
Michael Nygard's Release It! (first published in 2007) made the circuit breaker and bulkhead patterns popular for software. The book is based on real production failures, such as one slow integration that froze a whole retail site. Martin Fowler's short article "CircuitBreaker" made the pattern widely known.
Netflix Hystrix. Netflix built Hystrix (a Java library) around 2012. It wrapped every call to a dependency with timeouts, circuit breakers, bulkheads (separate thread pools) and fallbacks. Netflix had hundreds of dependencies, so something was always failing. Hystrix was later put into maintenance mode (no new features). Its own page now points to newer tools like Resilience4j and adaptive concurrency limits. The ideas live on everywhere.
Envoy and service meshes. Envoy (post 13) includes circuit-breaking limits (maximum connections, pending requests, retries) and outlier detection. Meshes like Istio let you set these in configuration. Many companies get basic bulkheads and breakers "for free" through their mesh.
Degraded pages on big sites. When a major e-commerce or streaming site has trouble, you often see parts of pages disappear (recommendations, "customers also bought", reviews) while browsing and checkout keep working. This is circuit breakers and fallbacks doing their job.
Interview Questions
Q1Explain the three states of a circuit breaker.
Closed: calls pass through, and failures and slow calls are counted in a rolling window. Open: when a threshold is crossed, calls fail at once without touching the dependency, for a cool-down period. Half-open: a few trial calls are allowed. If they succeed, the breaker closes. If they fail, it opens again.
Q2What's the difference between a timeout and a circuit breaker?
A timeout limits how long one call may wait. A circuit breaker looks at many calls. When a dependency is clearly unhealthy, it stops making calls at all. This saves threads and gives the dependency room to recover.
Q3What is a bulkhead?
It means keeping resources apart for each dependency, workload or tenant. You use separate thread pools, concurrency limits, connection pools, queues or instances. Then one slow or failing part can use up only its own share, not the whole service.
Q4What makes a good fallback?
An honest, simpler experience for non-critical features: cached or popular items instead of recommendations, hidden reviews, default avatars, or the last known exchange rate with a timestamp. For critical actions like payments, show a clear error and never fake success.
Q5How would you size a bulkhead?
With Little's Law: concurrency ≈ requests per second to that dependency × its normal latency, plus some extra room. If it is too small, it rejects normal busy times. If it is too large, it stops protecting everyone else.
Q6What's the risk of circuit breakers in production?
A quiet fallback can hide an outage for hours. Badly tuned thresholds cause false trips, or trip too late. Send an alert whenever a breaker opens, require a minimum number of calls before it trips, and test fallbacks regularly.
Key Takeaways
- A circuit breaker watches failures and slow calls. It opens to fail fast, then goes half-open to test recovery, and closes when the dependency is healthy.
- Always pair breakers with sensible fallbacks, and never fake success for critical actions like payments.
- A bulkhead gives each dependency, tenant or workload its own limited pool, so one failure cannot use up everything.
- Combine timeouts + limited retries + circuit breakers + bulkheads + fallbacks, in code (Resilience4j, Polly…) and/or in the mesh (Envoy, Istio).
- Alert when breakers open, size bulkheads with Little's Law, and test fallbacks.
Further Reading
- Martin Fowler's article "CircuitBreaker"
- Azure Architecture Center patterns "Circuit Breaker" and "Bulkhead"
- Release It! (2nd edition) by Michael Nygard (the chapters on "Circuit Breaker" and "Bulkheads")
- The Resilience4j documentation
- The Netflix Hystrix wiki page "How it Works" (for the history and ideas)
- The Envoy documentation on circuit breaking and outlier detection
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series