Circuit Breaker: What It Is, How It Works, and How to Implement It in Production

A poorly configured Circuit Breaker doesn't protect the system. It just trades a slow outage for a stream of rejections that are hard to explain.

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:

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:

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:

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.

Circuit Breaker state machine State machine of a Circuit Breaker with transitions between Closed, Open, and Half-Open based on failure rates, wait time, and trial calls. Closed Lets traffic through · records outcomes Open Rejects calls · protects capacity Half-Open Allows a few probes · validates failure or slow-call rate ≥ threshold + min. sample wait duration elapses probes OK · recovery probes fail · relapse
Circuit Breaker state machine. Four transitions define its behavior: the opening condition (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:

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:

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:

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:

These usually shouldn't count the same way:

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:

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:

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:

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:

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:

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.

PatternCore decision
TimeoutHow long a single attempt may wait
RetryWhen it's worth trying again
Circuit BreakerWhen to stop executing temporarily
BulkheadHow much capacity a dependency may consume
Rate limitingHow much demand is accepted over a period
DeadlineHow 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

YAML
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:

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

Java
@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:

  1. The call ran and failed.
  2. 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:

It shouldn't:

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.

C#
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:

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:

With the Micrometer binder, Resilience4j exposes metrics equivalent to:

Metrics
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:

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:

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:

  1. Does the operation cross a remote boundary or consume a resource that can degrade persistently?
  2. Does continuing to call add pressure or hold scarce capacity?
  3. Can we tell technical failures apart from functional rejections?
  4. Is there enough traffic to evaluate a sample?
  5. What will the consumer do when the call is rejected?
  6. Does the operation support fallback, degradation, or deferred execution?
  7. What timeout limits each attempt?
  8. How is concurrency constrained?
  9. What metrics will prove the circuit protects the system?
  10. 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:

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

Jorel del Portal

Jorel del Portal

Jorel del Portal is a systems engineer specialized in architecture, integration, resilience and observability of critical platforms. He designs systems, builds products and documents real engineering decisions.