Circuit Breaker vs Bulkhead Pattern: Which You Need
A circuit breaker stops calls to a failing dependency. A bulkhead stops one slow dependency from starving the rest. Here is how they combine with timeouts and retries, with real config.

10 min read · Last updated: October 24, 2026
The circuit breaker vs bulkhead pattern question has a short answer. A circuit breaker stops you from calling a dependency that keeps failing. A bulkhead caps how many resources one dependency can use, so a slow one cannot starve the rest. You usually want both, wrapped around a timeout, with retries outside them.
Came here from Instagram, LinkedIn, X or YouTube? Here is the full source list you came for. Everything below is built from these, nothing else.
The original patterns
- Martin Fowler, CircuitBreaker, March 6, 2014
- Microsoft Azure Architecture Center, Circuit Breaker pattern (updated 2025)
- Microsoft Azure Architecture Center, Bulkhead pattern (updated March 2026)
The library docs
- Resilience4j, CircuitBreaker docs
- Resilience4j, Bulkhead docs
- Resilience4j, Spring Boot getting started
- Netflix Hystrix README (maintenance mode notice)
- Netflix Hystrix wiki, How it Works
- Envoy, Circuit breaking
Timeouts, retries and cascading failures
- AWS Builders' Library, Marc Brooker, Timeouts, retries and backoff with jitter, December 5, 2019
- AWS Architecture Blog, Marc Brooker, Exponential Backoff and Jitter, March 4, 2015
- Google SRE book, Chapter 22, Addressing Cascading Failures
Service discovery
Key takeaways
- A circuit breaker fails fast after failures cross a threshold. Resilience4j's default opens at a 50% failure rate over the last 100 calls.
- A bulkhead isolates resources per dependency. Resilience4j's semaphore bulkhead defaults to 25 concurrent calls.
- Retries at every layer multiply load. AWS shows that 5 layers retrying 3 times each put 243x the load on the database.
- Netflix ran 10+ billion thread-isolated Hystrix calls per day, at a measured cost of 3 ms at p90 and 9 ms at p99.
- Hystrix is in maintenance mode at version 1.5.18. Netflix points new projects to Resilience4j.
- The safe stack: timeout first, bulkhead and breaker around it, retry with jittered backoff on the outside, and a retry budget on top.
Circuit breaker vs bulkhead pattern: the comparison table
Both patterns exist for one reason: to stop one bad dependency from taking your whole service down. They just watch different signals.
| Circuit breaker | Bulkhead | |
|---|---|---|
| Failure it prevents | Hammering a dependency that is already down | One slow dependency eating every thread or connection |
| What it watches | Failure rate and slow call rate | Number of calls in flight |
| What it does | Rejects calls instantly while open | Rejects or queues calls once the pool is full |
| State over time | Yes: closed, open, half-open | No, just a counter or a pool |
| Helps the dependency recover | Yes, it stops sending traffic | Partly, it caps traffic |
| Protects the caller | Yes, after it trips | Yes, from the first slow call |
| Typical tools | Resilience4j, Polly, Envoy | Resilience4j, Polly, separate pools or containers |
| Resilience4j defaults | 50% failure rate, 100 call window, 60 s open | 25 concurrent calls, 0 ms wait |
The short version: a circuit breaker reacts to errors, a bulkhead reacts to load. A breaker needs failures to happen before it trips. A bulkhead protects you before anything has failed.
What is a circuit breaker in microservices?
A circuit breaker in microservices is a wrapper around a remote call that counts failures. Once failures cross a threshold, it "opens" and returns an error immediately instead of calling the dependency. After a wait, it lets a few test calls through. If they succeed it closes again. If they fail it stays open.
Martin Fowler described it in March 2014 and credits Michael Nygard's book Release It! for popularizing it. His definition is short: "You wrap a protected function call in a circuit breaker object, which monitors for failures."
The three states, per Fowler and Microsoft's Azure Architecture Center:
- Closed. Calls go through. Failures are counted.
- Open. Calls fail immediately, without touching the dependency.
- Half-open. After a timer, a limited number of trial calls go through. Success closes the breaker. Any failure reopens it.
Microsoft's docs explain why half-open matters: it "helps prevent a recovering service from suddenly being flooded with requests." A service that just came back often cannot take full traffic yet.
Resilience4j, the open-source Java fault tolerance library, ships these defaults:
| Property | Default |
|---|---|
failureRateThreshold | 50% |
slowCallRateThreshold | 100% |
slowCallDurationThreshold | 60,000 ms |
slidingWindowType | COUNT_BASED |
slidingWindowSize | 100 calls |
minimumNumberOfCalls | 100 |
waitDurationInOpenState | 60,000 ms |
permittedNumberOfCallsInHalfOpenState | 10 |
Look at the slow call settings. A breaker can trip on latency, not just exceptions. That matters, because slow is often worse than down.
What is the bulkhead pattern?
The bulkhead pattern splits your resources into isolated pools, one per dependency or per consumer, so that if one pool is exhausted the others keep working. It is named after the walls inside a ship's hull: if one section floods, the ship stays afloat. It stops a single failure from spreading.
Microsoft's Bulkhead page describes the exact failure it prevents. A consumer calls an unresponsive service. Each request holds a connection while it waits. Eventually "the client's connection pool might be exhausted," and then the consumer "can't send requests to any other services, not only the original unresponsive service."
That is the whole point. Your payment service is down, and suddenly your product page will not load either, because both shared one connection pool.
The Netflix Hystrix wiki answers the common question of how Hystrix used this pattern. Hystrix "uses separate, per-dependency thread pools," so latency in one dependency can only exhaust its own pool. Most pools in the Netflix API were set to 10 threads, within a range of 5 to 20.
Resilience4j offers two kinds:
| Type | How it isolates | Defaults |
|---|---|---|
SemaphoreBulkhead | Caps concurrent calls on the caller's own thread | maxConcurrentCalls 25, maxWaitDuration 0 |
FixedThreadPoolBulkhead | Runs calls on a bounded pool with a queue | maxThreadPoolSize = available processors, queueCapacity 100 |
Bulkheads also work at the infrastructure level. Microsoft suggests separate containers, VMs or processes per service, and separate queues for async work. Their Kubernetes example gives one service its own CPU and memory limits, so it cannot starve its neighbours.
Circuit breaker vs retry vs timeout: what is the difference?
A timeout limits how long one call can wait. A retry tries a failed call again, betting the fault was brief. A circuit breaker stops calling once failures look persistent. Timeouts make failures visible, retries handle short blips, and breakers handle longer outages. They solve different problems and work best together.
Microsoft's docs put the retry and breaker difference plainly. Retry expects the operation to eventually succeed. A circuit breaker "prevents an application from performing an operation that's likely to fail." When you combine them, the retry logic should stop when the breaker says the fault is not transient.
| Pattern | Question it answers | Without it |
|---|---|---|
| Timeout | How long do I wait? | Threads hang on a dead call |
| Retry with backoff | Was that just a blip? | One dropped packet becomes a user error |
| Circuit breaker | Is this dependency down? | You pay the full timeout on every call |
| Bulkhead | How much can this dependency take from me? | One slow call path starves everything |
Timeouts come first. Microsoft warns that a breaker cannot fully protect you if the timeout is too long, because threads stay blocked before the breaker ever records a failure. Marc Brooker at AWS suggests choosing an acceptable false timeout rate, such as 0.1%, and setting the timeout at the matching latency percentile of the dependency, p99.9.
How do retries cause cascading failures?
Retries cause cascading failures by multiplying load on a service that is already struggling. Each layer retries on its own, so attempts stack up: AWS's example of five layers retrying three times each sends 243x the load to the database. The overloaded service gets even more traffic, fails more, and triggers more retries.
Google's SRE book defines a cascading failure as "a failure that grows over time as a result of positive feedback." Its example: a backend that handles 10,000 queries per second receives 10,100. Some requests get rejected, clients retry, and the retries become new load. The problem can continue even after the original spike is gone. The same chapter shows stacked retries turning one user action into 64 attempts on the database.
Brooker calls retries "selfish." They spend more of the server's time to raise your own chance of success. The fixes, from AWS and Google:
- Retry at one layer only. Not at every hop.
- Cap the attempts. Never retry forever.
- Use exponential backoff with jitter. Brooker's 2015 simulation found plain exponential backoff, without jitter, did the most work and took the most time. Jitter spreads retries so clients do not all fire together.
- Set a retry budget. Google's example allows 60 retries per minute per process, then fails requests instead of retrying.
- Retry only idempotent calls. Brooker: "APIs with side effects aren't safe to retry unless they provide idempotency."
- Propagate deadlines. Pass the remaining time down the call chain, so servers stop working on requests the client already gave up on.
Here is a capped exponential backoff with full jitter in TypeScript:
async function withRetry<T>(
fn: () => Promise<T>,
maxAttempts = 3,
baseMs = 100,
capMs = 2000,
): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt >= maxAttempts) throw err;
const ceiling = Math.min(capMs, baseMs * 2 ** attempt);
await new Promise((r) => setTimeout(r, Math.random() * ceiling));
}
}
}
The random sleep between zero and the ceiling is what stops a thousand clients from retrying in the same millisecond.
How to prevent cascading failures with one config
To prevent cascading failures, put a timeout on every remote call, isolate each dependency with a bulkhead, add a circuit breaker that trips on errors and slow calls, and retry at one layer only with jittered backoff and a budget. On the server side, shed load early so overload gets rejected cheaply instead of queued.
Resilience4j documents the default order it wraps these in, in Spring Boot:
Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ) ) )
Retry is outermost. So when the breaker is open, the retry gets a fast rejection instead of wasting attempts on a dependency that is known to be down. Here is the example application.yml from the Resilience4j docs:
resilience4j:
circuitbreaker:
circuitBreakerAspectOrder: 1
instances:
backendA:
slidingWindowSize: 100
failureRateThreshold: 50
waitDurationInOpenState: 10s
retry:
retryAspectOrder: 2
instances:
backendA:
maxAttempts: 3
waitDuration: 500ms
bulkhead:
bulkheadAspectOrder: 0
instances:
backendA:
maxConcurrentCalls: 10
maxWaitDuration: 10ms
timelimiter:
timeLimiterAspectOrder: 0
instances:
backendA:
timeoutDuration: 2s
cancelRunningFuture: true
It needs resilience4j-spring-boot3, spring-boot-starter-actuator and spring-boot-starter-aop. Read it like this: each call gets 2 seconds, at most 10 run at once, the breaker opens at 50% failures over 100 calls and waits 10 seconds before testing again, and retry makes up to 3 attempts.
For the server side, Google's SRE book suggests returning HTTP 503 once in-flight requests pass a set threshold. A cheap rejection is far better than a slow timeout.
Hystrix alternatives: is Resilience4j the replacement?
Yes, for Java. Netflix's README says Hystrix "is no longer in active development, and is currently in maintenance mode," with 1.5.18 as the final release. Netflix uses active projects like Resilience4j for new internal work and recommends others do the same. For .NET, Microsoft's docs point to Polly.
Your options, depending on where you want the logic to live:
| Option | Where it runs | Notes from the docs |
|---|---|---|
| Hystrix | In process, Java | Maintenance mode, final version 1.5.18 |
| Resilience4j | In process, Java | Breaker, bulkhead, retry, rate limiter, time limiter |
| Polly | In process, .NET | Named by Microsoft for consumer bulkheads |
| Envoy or a service mesh | Sidecar proxy | On by default, e.g. 1,024 connections per cluster |
One detail most people miss: what Envoy, the open-source proxy, calls "circuit breaking" is mostly a bulkhead. Its thresholds are max connections, max pending requests, max requests and max active retries per upstream cluster. Those are concurrency caps. Envoy's docs also say its circuit breaking is "fully distributed (not coordinated)," so each proxy enforces its limits on its own.
Is the thread pool cost real? Netflix measured one command at 60 requests per second: no cost at the median, 3 ms at the 90th percentile, 9 ms at the 99th. They still ran 10+ billion of these calls a day, with 40+ thread pools per API instance.
What is service discovery and why do I need it?
Service discovery is how one service finds a working network address for another when instances keep changing. Instances register with a registry, and callers look them up by name instead of a hardcoded IP. You need it because containers and pods get new addresses every time they restart, move or scale.
The Kubernetes docs state the problem directly: "Pods are ephemeral resources," and the set of Pods running "could be different from the set of Pods running that application a moment later." A Kubernetes Service gives callers one stable name for that changing set.
Chris Richardson's microservices.io describes the client-side version: the client queries a service registry and picks an instance itself. In the Netflix stack, Eureka is the registry and Ribbon is the HTTP client that resolves names like REGISTRATION-SERVICE through it.
Two terms that confuse people, as Envoy defines them: downstream is whoever connects to you. Upstream is the backend you forward requests to. Circuit breakers and bulkheads protect you from your upstreams.
Which service discovery errors trigger circuit breakers?
Discovery errors trip breakers when the caller cannot resolve or reach a healthy instance, so calls fail or time out and push up the failure rate. The usual causes are a registry that is unreachable, instances that are not registered yet, and misconfigured registry URLs. The breaker cannot tell a discovery bug from a real outage.
With Spring Cloud Netflix Eureka, three documented behaviours explain most of these:
- Registration is slow. Heartbeats default to 30 seconds, and a service is not discoverable until the instance, server and client caches agree, "so it could take 3 heartbeats." Calls in that window fail.
- Client HTTP timeouts default to 3 minutes. If the registry is unreachable, threads can block for a long time. Set the
eureka.client.restclient.timeoutvalues explicitly. - A lone Eureka server still acts as a client. Without a peer URL, the docs say it "fills your logs with a lot of noise about not being able to register with the peer." The standalone fix from the Spring docs:
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
This is the first thing to check when a Eureka client throws transport errors about not reaching any known server. Also note that defaultZone is case sensitive and must be camel case, per the Spring docs.
Which should you pick? Circuit breaker or bulkhead, by situation
Pick based on the failure you fear most. If a dependency goes fully down and you keep wasting time on it, you need a circuit breaker. If one slow dependency can drain shared threads or connections, you need a bulkhead. Any service that calls several remote dependencies in production should run both.
| Your situation | Pick |
|---|---|
| One dependency is flaky and sometimes down for minutes | Circuit breaker, with a cached or default fallback |
| Many dependencies share one thread or connection pool | A bulkhead per dependency |
| One noisy tenant or client can overload you | A bulkhead per consumer (separate pools or instances) |
| Calls fail for a moment, then work | Retry with jittered backoff, inside a budget |
| Calls hang with no error at all | Timeout first, then a breaker with slow call thresholds |
| You already run a service mesh | Use the mesh limits, keep app-level fallbacks |
| You are calling local in-memory code | Neither. Microsoft says a breaker only adds overhead there |
If you want to see how large systems stay up under huge load, read how Netflix streams videos without buffering and how WhatsApp handles billions of messages. If this is interview prep, these patterns come up all the time in system design interviews.
FAQ
Can I use a circuit breaker and a bulkhead together?
Yes, and you usually should. Resilience4j's default order wraps Retry, then CircuitBreaker, then TimeLimiter, then Bulkhead around your function. The bulkhead caps concurrency and the breaker stops calls once failures pile up. Microsoft's docs also recommend combining bulkheads with retry and circuit breakers.
What happens when a circuit breaker is open?
Calls fail immediately without reaching the dependency. In Resilience4j they are rejected with a CallNotPermittedException. You can return a cached or default value instead, as Microsoft's example does. After the wait duration, the breaker moves to half-open and tests a few calls.
Is Hystrix still safe to use?
It still runs, but Netflix stopped active development and will not review issues or release new versions after 1.5.18. For new Java projects, Netflix itself recommends active projects like Resilience4j. If you are on Hystrix today, plan the migration.
Should I retry inside the circuit breaker or outside it?
Outside. Resilience4j's default order puts Retry outermost, so each attempt passes through the breaker. When the breaker is open, the retry gets a fast rejection, and Microsoft says retry logic should stop once the breaker signals a fault that is not transient.
Audit one service this week: list every remote call and check that each has a timeout, a bulkhead and a breaker. Then explore the free tools on Art of Code, or browse developer jobs where this knowledge gets you hired.
Written by Adithya Guttha, Founder of Art of Code.
Sign in to like and save posts.
Keep reading



Discussion
Comments (0)
Want to join the conversation?
Sign in to commentNo comments yet. Be the first to comment!