Back to Blog
EngineeringOctober 24, 202616 min read

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.

Circuit Breaker vs Bulkhead Pattern: Which You Need

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

The library docs

Timeouts, retries and 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 breakerBulkhead
Failure it preventsHammering a dependency that is already downOne slow dependency eating every thread or connection
What it watchesFailure rate and slow call rateNumber of calls in flight
What it doesRejects calls instantly while openRejects or queues calls once the pool is full
State over timeYes: closed, open, half-openNo, just a counter or a pool
Helps the dependency recoverYes, it stops sending trafficPartly, it caps traffic
Protects the callerYes, after it tripsYes, from the first slow call
Typical toolsResilience4j, Polly, EnvoyResilience4j, Polly, separate pools or containers
Resilience4j defaults50% failure rate, 100 call window, 60 s open25 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:

  1. Closed. Calls go through. Failures are counted.
  2. Open. Calls fail immediately, without touching the dependency.
  3. 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:

PropertyDefault
failureRateThreshold50%
slowCallRateThreshold100%
slowCallDurationThreshold60,000 ms
slidingWindowTypeCOUNT_BASED
slidingWindowSize100 calls
minimumNumberOfCalls100
waitDurationInOpenState60,000 ms
permittedNumberOfCallsInHalfOpenState10

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:

TypeHow it isolatesDefaults
SemaphoreBulkheadCaps concurrent calls on the caller's own threadmaxConcurrentCalls 25, maxWaitDuration 0
FixedThreadPoolBulkheadRuns calls on a bounded pool with a queuemaxThreadPoolSize = 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.

PatternQuestion it answersWithout it
TimeoutHow long do I wait?Threads hang on a dead call
Retry with backoffWas that just a blip?One dropped packet becomes a user error
Circuit breakerIs this dependency down?You pay the full timeout on every call
BulkheadHow 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:

  1. Retry at one layer only. Not at every hop.
  2. Cap the attempts. Never retry forever.
  3. 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.
  4. Set a retry budget. Google's example allows 60 retries per minute per process, then fails requests instead of retrying.
  5. Retry only idempotent calls. Brooker: "APIs with side effects aren't safe to retry unless they provide idempotency."
  6. 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:

OptionWhere it runsNotes from the docs
HystrixIn process, JavaMaintenance mode, final version 1.5.18
Resilience4jIn process, JavaBreaker, bulkhead, retry, rate limiter, time limiter
PollyIn process, .NETNamed by Microsoft for consumer bulkheads
Envoy or a service meshSidecar proxyOn 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.timeout values 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 situationPick
One dependency is flaky and sometimes down for minutesCircuit breaker, with a cached or default fallback
Many dependencies share one thread or connection poolA bulkhead per dependency
One noisy tenant or client can overload youA bulkhead per consumer (separate pools or instances)
Calls fail for a moment, then workRetry with jittered backoff, inside a budget
Calls hang with no error at allTimeout first, then a breaker with slow call thresholds
You already run a service meshUse the mesh limits, keep app-level fallbacks
You are calling local in-memory codeNeither. 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.

#circuit breaker#bulkhead pattern#microservices#resilience4j#system design

Sign in to like and save posts.

Curious Adithya

Written by

Curious Adithya

@curious_adithya · 5 followers

Discussion

Comments (0)

Want to join the conversation?

Sign in to comment

No comments yet. Be the first to comment!