The pattern exists to stop an application from sending more work to a dependency that has already proven to be degraded. When it detects a high enough proportion of errors or slow calls, it opens the circuit and temporarily rejects new executions. It then allows a limited number of trial calls to decide whether the dependency can take traffic again.
The idea sounds simple. The implementation isn't.
To make the pattern useful, you have to answer at least these questions:
- Which outcomes count as a failure?
- How many calls make up a representative sample?
- What does it mean for a call to be slow?
- How long should the circuit stay open?
- How many requests will be used to test recovery?
- What will the user see when a call is rejected?
- How will the team know the circuit is protecting the system rather than masking another problem?
A Circuit Breaker isn't a switch you flip and forget. It's an operational policy expressed in code.
What a Circuit Breaker solves
Suppose an application calls an external service that normally responds in 200 ms. The dependency starts to degrade, and every request now takes eight seconds before ending in a timeout.
Without a protection policy, the client keeps sending calls. While it waits, it:
- holds connections open;
- ties up threads or async tasks;
- deepens queue backlogs;
- consumes memory;
- raises latency for other operations;
- can trigger retries that multiply the load;
- ends up spreading the degradation to components that were healthy.
The original error belongs to one dependency. The resulting incident now belongs to the whole system.
The Circuit Breaker introduces a decision: when the evidence says that continuing to call has a low chance of success and a high cost, it stops running the operation for a while.
Instead of waiting eight seconds for another timeout, the application rejects the call immediately. That fast response doesn't heal the dependency, but it stops burning capacity on an operation that is unlikely to end well.
Failing fast isn't the same as hiding the failure
Opening the circuit doesn't make the dependency healthy. It doesn't erase the functional impact either.
The pattern protects resources and limits propagation. The application still has to decide what to do with the rejected operation:
- return an explicit error;
- serve previously stored data and flag it as possibly stale;
- degrade a non-critical feature;
- queue work for later processing, if the semantics allow it;
- block an action that can't be performed safely.
The worst outcome is confirming an operation that never happened.
The three states of a Circuit Breaker
A Circuit Breaker behaves like a state machine. Libraries may add administrative or observability states, but the normal behavior revolves around three.
Closed → Open when the failure or slow-call rate exceeds the threshold with a sufficient
minimum sample); the wait time (Open → Half-Open once the wait duration
elapses); and the trial calls that decide recovery
(Half-Open → Closed) or relapse (Half-Open → Open).
Closed
In the Closed state, calls pass through to the dependency.
The Circuit Breaker records the outcome and duration of each execution within a window. As long as the proportion of failures and slow calls stays below the configured thresholds, the circuit stays closed.
Closed doesn't mean the dependency never fails. It means the available evidence doesn't yet justify cutting off traffic.
Open
When the failure or slow-call rate exceeds the threshold and there's a sufficient minimum sample, the circuit moves to Open.
In this state, new calls never reach the dependency. They're rejected locally.
This lets you:
- free up client resources;
- reduce pressure on the degraded service;
- avoid waits that no longer add value;
- give an automatic recovery or an operator intervention time to take effect.
The circuit stays open for a defined interval. That time should not be read as “how long the dependency takes to recover.” It's just the period after which it's worth checking again.
Half-Open
After the wait interval, the Circuit Breaker enters Half-Open and allows a limited number of trial calls.
You shouldn't send all traffic back at once. Recovery can be partial, unstable, or temporary.
If the trial calls meet the health criteria, the circuit returns to Closed. If they fail or are still too slow, it goes back to Open.
Half-Open is a verification stage, not a declaration of recovery.
How it decides to open
The configuration has to reflect the expected behavior of one specific dependency. Copying values from another service is a fast way to get a pattern that reacts too late, too early, or for the wrong reasons.
Count-based window
A count-based window evaluates the last N executions.
Example:
- window size: 100 calls;
- minimum sample: 50 calls;
- failure threshold: 50%.
The Circuit Breaker can open when at least half of the calls in the evaluated sample fail.
This model is easy to reason about when traffic is relatively stable. In bursty systems, those 100 calls might span several minutes or just a fraction of a second.
Time-based window
A time-based window evaluates the calls that occurred over the last N seconds.
Example:
- window: 30 seconds;
- minimum sample: 20 calls;
- failure threshold: 50%.
This approach aligns the evaluation with an operational period, but it still needs a minimum volume. Without that requirement, two failures inside a low-traffic window could open the circuit on a statistically weak sample.
Failure rate threshold
The failure rate threshold defines what proportion of calls recorded as failed triggers the opening.
The critical point isn't picking 40%, 50%, or 60%. It's correctly defining what gets recorded as a failure.
These can usually count:
- network errors;
- timeouts;
- specific 5xx responses;
- functional results that represent technical unavailability.
These usually shouldn't count the same way:
- validation errors;
- malformed requests;
- invalid credentials;
- business rules that were correctly rejected;
- cancellations caused because the consumer abandoned the operation.
If every exception raises the error rate, the circuit can open because of a client-side problem or a functional condition the dependency handled correctly.
Slow call rate threshold
A dependency doesn't need to return errors to be dangerous. It can respond correctly, but do it so late that it ties up resources, blows deadlines, and drags down the entire chain.
The slow call rate threshold lets the circuit open when a percentage of calls exceeds a defined duration.
To use it, you first have to decide what “slow” means for that operation. A query that normally takes 50 ms shouldn't share the same limit as a process designed to finish in five seconds.
The threshold should relate to:
- the operation's SLO;
- the end-to-end latency budget;
- the per-attempt timeout;
- the pool or queue capacity;
- the cost of keeping an execution in flight.
You can dig into this relationship in latency in distributed systems.
Minimum number of calls
The minimum sample keeps you from making a drastic decision on thin evidence.
Imagine a service that receives four calls per minute. If the first two fail and the threshold is 50%, opening immediately can block the next requests for a long stretch, even if the dependency has already recovered.
At the other extreme, a minimum sample that's too large can keep the circuit from opening during a real degradation.
How many observations do I need to tell an isolated failure apart from a persistent condition without reacting too late?
Wait duration in the Open state
The time in Open should be long enough to avoid constant probing, but not so long that it prolongs an outage after the dependency has recovered.
It can be derived from:
- typical recovery times;
- autoscaling behavior;
- restarts or failovers;
- maintenance windows;
- dependency limits;
- the cost of a failed probe.
A fixed interval is a starting point, not a universal truth. In advanced systems it can adapt to consecutive failures or external signals, but that complexity is only worth it if there's evidence it improves behavior.
Permitted calls in Half-Open
A single successful call may be too little to declare recovery. A hundred trial calls can re-saturate a service that's still fragile.
The number should be small but representative of the traffic and the protected operations.
The kind of request used as a probe matters too. A lightweight operation doesn't guarantee that the more expensive critical path is healthy.
Configuration depends on traffic
There's no universal configuration, because the same set of values produces different behavior depending on volume.
High-volume dependencies
On a dependency with thousands of calls per second, a small window fills almost instantly. The circuit can react fast, but it can also become sensitive to brief spikes.
It's worth evaluating:
- time-based windows;
- slow-call thresholds;
- segmentation by operation;
- concurrency isolation;
- the impact of coordinated openings across many instances.
Low-volume dependencies
On a dependency with few calls, a large window can take too long to gather the minimum sample. The circuit might not open during an obvious failure.
In these cases you may need:
- a smaller window;
- complementary health signals;
- strict timeouts;
- a manual or external isolation strategy;
- separating operations with different profiles.
Bursty traffic
A brief spike can fill a count-based window and dominate the decision. A time-based window can represent the degradation period better, as long as the minimum sample is controlled.
Asynchronous processing
On a queue consumer, rejecting a call immediately can cause redeliveries, move messages to an error queue, or speed up a retry loop.
The Circuit Breaker has to coordinate with:
- message visibility;
- the broker's retry policy;
- the dead-letter queue;
- idempotency;
- backpressure;
- the ability to pause consumers.
The pattern can't be designed in isolation from the processing semantics.
A Circuit Breaker isn't Bulkhead, Timeout, or Retry
These patterns can be part of the same policy, but they aren't interchangeable.
| Pattern | Core decision |
|---|---|
| Timeout | How long a single attempt may wait |
| Retry | When it's worth trying again |
| Circuit Breaker | When to stop executing temporarily |
| Bulkhead | How much capacity a dependency may consume |
| Rate limiting | How much demand is accepted over a period |
| Deadline | How much total time the end-to-end operation has |
A Circuit Breaker doesn't limit the number of concurrent calls while it's Closed. If a hundred requests are allowed through at the same time, all hundred can reach the dependency. The window size is not a concurrency limit.
To contain threads, connections, or in-flight tasks you need a Bulkhead, a dedicated pool, a semaphore, a bounded queue, or another equivalent mechanism.
It doesn't run retries either. It only observes outcomes and decides whether to allow new executions.
Composition and order matter because they determine whether the circuit observes each individual attempt or only the final result. That problem is covered separately in Timeout, Retry, and Circuit Breaker: how to combine them.
A reasoned implementation with Resilience4j
The example below uses Spring Boot and Resilience4j. The values are illustrative. Don't copy them into production without measuring the dependency's behavior.
Configuration
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowType: TIME_BASED
slidingWindowSize: 30
minimumNumberOfCalls: 20
failureRateThreshold: 50
slowCallRateThreshold: 50
slowCallDurationThreshold: 2s
permittedNumberOfCallsInHalfOpenState: 5
waitDurationInOpenState: 20s
automaticTransitionFromOpenToHalfOpenEnabled: true
eventConsumerBufferSize: 50
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
ignoreExceptions:
- com.example.domain.BusinessRuleException
instances:
catalogService:
baseConfig: default
This configuration expresses a concrete policy:
- it evaluates calls from the last 30 seconds;
- it doesn't compute rates until it has gathered at least 20 executions;
- it opens if 50% fail;
- it also opens if 50% exceed two seconds;
- it stays 20 seconds in
Open; - it uses five calls to verify recovery;
- it doesn't penalize the service for a business rule that ran correctly.
The configuration is still incomplete if there's no timeout on the protected call. Without a timeout, an execution can stay blocked for a long time before it's recorded as slow or failed.
Protected call
@Service
public class CatalogGateway {
private final CatalogClient client;
private final CircuitBreaker circuitBreaker;
public CatalogGateway(
CatalogClient client,
CircuitBreakerRegistry registry) {
this.client = client;
this.circuitBreaker = registry.circuitBreaker("catalogService");
}
public Product findProduct(String productId) {
Supplier<Product> protectedCall = CircuitBreaker.decorateSupplier(
circuitBreaker,
() -> client.findProduct(productId)
);
try {
return protectedCall.get();
} catch (CallNotPermittedException exception) {
throw new CatalogTemporarilyUnavailableException(
"Catalog service is temporarily unavailable",
exception
);
}
}
}
The code distinguishes two situations:
- The call ran and failed.
- The call never even ran, because the circuit was open.
That difference matters for logs, metrics, responses to the consumer, and retry policies.
Fallback: degrade without lying
A valid fallback depends on the semantics.
For a catalog lookup, returning a cached copy with a staleness marker might be acceptable. For a payment order, confirming success without running the operation would be wrong.
A fallback can:
- return cached data;
- reduce the level of detail;
- disable a secondary feature;
- respond with temporary unavailability;
- queue an order, only if the contract allows deferred execution.
It shouldn't:
- invent a successful result;
- hide data loss;
- silently turn a write into a stale read;
- keep metrics from reflecting the real impact.
A conceptual equivalent with Polly for .NET
The pattern doesn't depend on Java. Polly models the same decision through a failure ratio, a sampling duration, a minimum throughput, and a break period.
var options = new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 20,
BreakDuration = TimeSpan.FromSeconds(20),
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(response =>
(int)response.StatusCode >= 500)
};
var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddCircuitBreaker(options)
.Build();
The names change. The decisions are the same:
- what counts as a failure;
- over which window;
- with what minimum volume;
- for how long new executions are blocked.
The library implements the mechanism. The architecture is still the team's responsibility.
Metrics you need
A Circuit Breaker without telemetry can reduce load and, at the same time, make it harder to understand why users are getting rejections.
At a minimum, you need to observe:
- current state per dependency;
- transitions between states;
- the number and duration of openings;
- successful, failed, and ignored calls;
- calls rejected because the circuit was open;
- failure rate;
- slow call rate;
- call duration;
- the results of the
Half-Openprobes; - the protected operation or dependency;
- the impact on end-to-end latency and errors.
With the Micrometer binder, Resilience4j exposes metrics equivalent to:
resilience4j.circuitbreaker.state
resilience4j.circuitbreaker.calls
resilience4j.circuitbreaker.failure.rate
resilience4j.circuitbreaker.slow.call.rate
resilience4j.circuitbreaker.not.permitted.calls
resilience4j.circuitbreaker.buffered.calls
The final names may change depending on the metrics backend, but the operational information should stay the same.
Don't automatically turn every open circuit into a full outage
An open circuit can mean that a secondary feature is degraded while the rest of the application keeps running.
If the global health check flips to DOWN because of any open circuit, the orchestrator might pull healthy instances, trigger restarts, or reduce available capacity even further.
The health signal has to represent the real semantics:
- is the dependency critical to every operation?
- is there controlled degradation?
- should the load balancer pull the instance?
- do you need an alert without changing the process's readiness?
Health, alerting, and autoscaling shouldn't share an automatic reading of the same signal.
You can go deeper into signal design in observability with metrics, logs, and traces.
Recommended alerts
Not every opening needs to wake someone up. A brief opening can be exactly the expected behavior.
Alerts should account for duration, repetition, and impact.
Circuit open longer than expected
An opening that exceeds the normal recovery time means the dependency is still degraded or the trial calls aren't representative.
Repeated openings
Several Closed → Open transitions within a window can reveal instability, thresholds that are too sensitive, or a dependency that only partially recovers.
Oscillation between Open and Half-Open
The circuit tries to recover traffic, fails, and opens again. This oscillation can produce intermittent behavior for consumers.
Rising slow calls
The slow-call rate can foreshadow errors, saturation, and timeouts. Waiting for the final error throws away reaction time.
Rejections with functional impact
A thousand rejections on an optional feature aren't the same as ten rejections on a critical operation. The alert has to factor in the affected flow and its volume.
Several circuits open at once
This can point to a shared problem:
- the network;
- DNS;
- the service mesh;
- the database;
- an external provider;
- client-side saturation;
- a common configuration change.
Correlation keeps you from investigating each circuit as a separate incident. That same instinct shapes how you investigate open circuits during an incident: the shared pattern first, then each dependency.
Common mistakes
Copying thresholds from another system
Two services with different traffic, latency, and criticality shouldn't share values just because they use the same library.
Counting every exception as a failure
A valid business rule can end up degrading the technical rate and open the circuit even though the dependency is healthy.
Opening on an insufficient sample
Few calls produce unstable decisions, especially on low-volume operations.
Not protecting against slow calls
A service can return 200 OK and still destroy the latency budget.
Using a fallback that hides data loss
Degradation stops being resilience when it breaks the functional contract.
Retrying without limits before recording the result
The dependency takes on more load, and the Circuit Breaker can react too late because it only sees the final result of several attempts.
Sharing one circuit across different dependencies
If two endpoints have different profiles, a degradation in one can needlessly block the other.
Confusing window size with concurrency
A 50-call window doesn't stop 500 executions from running in parallel. That takes capacity isolation.
Not instrumenting transitions
The team sees errors but doesn't know when the circuit opened, why it did, or how much traffic it rejected.
Treating Open as recovery
Open only stops calls. The dependency keeps failing until there's evidence to the contrary.
Confusing Circuit Breaker with rate limiting
The Circuit Breaker reacts to the observed health of a dependency. The rate limiter controls how much demand is accepted. One doesn't replace the other.
When not to use a Circuit Breaker
The pattern adds value when there's a remote dependency, a potentially persistent condition, and a real cost to continuing to execute.
It doesn't always meet those conditions.
Local operations
An in-memory function that fails because of a deterministic bug won't recover just because calls are blocked for 20 seconds.
Non-transient errors
An invalid configuration, an incompatible schema, or a revoked credential need fixing. Opening and probing periodically can add noise without changing the outcome.
Flows where blocking hurts consistency
In some coordinated processes, rejecting an intermediate operation without a compensation strategy can leave incomplete state.
Dependencies with insufficient volume
If there's never a representative sample, a decision based only on rates can be weak. Timeouts, health signals, manual controls, or a different model may be a better fit.
Fully decoupled processes
A queue-based flow may need consumer pausing, backpressure, broker retries, and dead-letter queues instead of a traditional Circuit Breaker around each message.
When the dependency already offers proper rejection semantics
An SDK or gateway may already include throttling, retry hints, and health control. Adding another layer without understanding the existing one can duplicate policies and create unexpected interactions.
Decision framework
Before implementing the pattern, answer:
- Does the operation cross a remote boundary or consume a resource that can degrade persistently?
- Does continuing to call add pressure or hold scarce capacity?
- Can we tell technical failures apart from functional rejections?
- Is there enough traffic to evaluate a sample?
- What will the consumer do when the call is rejected?
- Does the operation support fallback, degradation, or deferred execution?
- What timeout limits each attempt?
- How is concurrency constrained?
- What metrics will prove the circuit protects the system?
- What condition will justify closing the circuit again?
If these questions have no answers, there's no resilience policy yet. There's only one more dependency added to the project.
Conclusion
A Circuit Breaker doesn't make a dependency fail less. It makes the system stop behaving as if every new attempt had the same chance of success.
Its value shows up when it turns operational evidence into an explicit decision:
- allow;
- block;
- probe;
- recover;
- block again if the recovery isn't real.
A correct implementation doesn't start by choosing a library. It starts by defining what you're protecting, which outcomes matter, how much evidence you need, and what behavior is safe during degradation.
Then you code it. Then you instrument it. Finally, you validate it in production.
The case that sums it up best: on a high-availability payment platform, isolating dependencies and introducing circuit breakers with this framework reduced critical incidents by 70%. The number matters less than its cause: failures stopped propagating.
Technical references
- Resilience4j — CircuitBreaker: resilience4j.readme.io/docs/circuitbreaker
- Resilience4j — Spring Boot 2 and 3: resilience4j.readme.io/docs/getting-started-3
- Resilience4j — Micrometer metrics: resilience4j.readme.io/docs/micrometer
- Polly — Circuit breaker resilience strategy: pollydocs.org/strategies/circuit-breaker
- Microsoft Azure Architecture Center — Circuit Breaker pattern: learn.microsoft.com/azure/architecture/patterns/circuit-breaker