The Circuit Breaker Pattern: Containing Failure in Distributed Systems

How to protect the work around an unavailable service and decide when it is safe to try again.

  • Distributed Systems
  • Architecture
  • Reliability
The Circuit Breaker Pattern: Containing Failure in Distributed Systems

Key takeaways

  • A circuit breaker temporarily rejects calls when recent outcomes indicate that a dependency is unhealthy.
  • Failure classification, observation windows, and recovery probes need to reflect the operation being protected.
  • Timeouts, concurrency limits, and safe retry policies address risks that a circuit breaker cannot handle alone.
  • Fallbacks and recovery should preserve business rules and make the status of the user's work clear.

A customer submits an order, and the checkout service waits for a payment provider. A response that usually arrives quickly now takes several seconds. More customers reach the same step, more connections remain occupied, and retries add further requests. Before long, a payment problem is affecting parts of the shop that do not need to take a payment at all.

The initial fault belongs to one dependency. The wider disruption comes from how the surrounding system responds to it. Continuing to send work can consume the caller's resources while adding pressure to a service that is already struggling. A circuit breaker gives the caller a way to recognise that pattern and temporarily stop making those calls.

What the Breaker Controls

The circuit breaker pattern described by Microsoft places a decision around calls to a remote operation. It observes recent outcomes and rejects new calls when a configured threshold is reached. This can limit the spread of a dependency failure while giving the application a prompt result it can handle.

The usual model has three states:

StateWhat happens to a new callWhat changes the state
ClosedThe call reaches the dependency and its outcome is recorded.The configured failure threshold opens the circuit.
OpenThe call is rejected without reaching the dependency.After a waiting period, the breaker permits a recovery trial.
Half-openA limited number of calls test whether the dependency can respond.Their outcomes determine whether normal traffic resumes or the circuit opens again.

Opening the circuit does not repair the payment provider or undo a payment already submitted. It changes how subsequent calls are handled. The application still needs to decide what to tell the customer and what to do with the order.

Choosing What Counts as Failure

A declined card and an unreachable payment provider both prevent a purchase from completing, but they call for different responses. A valid decline shows that the provider processed the request and returned a business outcome. Counting every decline as an availability failure could block other customers from paying even while the provider is operating normally.

The team needs to classify the signals relevant to this particular operation. Connection errors, timeouts, and selected server errors may indicate an unhealthy dependency. Invalid input usually requires a correction by the caller. Rate limiting needs interpretation in the context of the provider's policy and the scope of the affected traffic.

Libraries make these choices concrete. Resilience4j's circuit breaker documentation describes configurable exception handling, count-based or time-based observation windows, minimum call counts, and thresholds for failed or slow calls. These settings determine what evidence the breaker uses before interrupting traffic.

For an illustrative policy, suppose the team evaluates the last 20 completed calls and opens the circuit when at least 10 have failed, once the full sample is available. It then waits 30 seconds before allowing a limited recovery trial. These numbers describe a policy to test, not a general recommendation. At low traffic, collecting 20 outcomes may take too long; during a sudden surge, many requests may already be waiting before the threshold is reached.

Timeouts and Capacity Still Matter

A timeout bounds how long the caller waits for an individual attempt. A breaker uses observed outcomes to decide whether later attempts should proceed. If requests remain stuck for too long, the caller can exhaust its resources before enough failures have been recorded to open the circuit.

Concurrency needs its own control too. As the Resilience4j documentation explains, a circuit breaker's observation window does not limit the number of simultaneous calls. A bulkhead, such as a dedicated connection pool or concurrency limit, can restrict how much capacity one dependency consumes. That separation matters for the shop: an unavailable payment provider should not occupy every resource needed to browse products or retrieve an existing order.

Retry decisions also belong within the overall request budget. Each additional attempt spends time and capacity. Retrying a call rejected by an open circuit without delay merely repeats a decision the system has already made.

A Timeout Can Leave the Outcome Unknown

Payments make an important limitation visible. The provider may accept a charge but lose the response before it reaches checkout. From the caller's perspective, the request timed out. From the provider's perspective, the work may be complete.

A circuit breaker cannot resolve that uncertainty. The retry design needs a way to avoid treating the same purchase as a new operation. AWS's discussion of making retries safe with idempotent APIs explains how a caller-supplied request identifier can let a service recognise repeated intent. That protection depends on the provider's guarantees and on the caller reusing the identifier correctly.

For this checkout, the order should retain enough information to reconcile the payment outcome. Showing a definitive failure and encouraging a fresh purchase could create a duplicate charge if the original request succeeded. The interface needs to distinguish an operation that was never sent from one whose result is still unknown.

Fallbacks Need a Business Decision

Some missing capabilities have a useful substitute. If personalised recommendations are unavailable, the shop might display a static selection. A payment cannot be replaced by a fabricated success response. Keeping the basket available and explaining that payment cannot currently begin may be the appropriate behaviour when the circuit rejects the call before submission.

Queueing work is another possible design, but it creates obligations. The application needs durable storage, deduplication, an expiry policy, and a clear account of what the user has agreed to wait for. An order accepted for later processing must not appear indistinguishable from a confirmed purchase.

This connects to the question behind local-first architecture: which parts of the user's work can continue independently? A useful fallback starts with that boundary and preserves what the application can honestly promise.

Recovery Needs Evidence

Testing should cover what happens after the circuit opens. For the shop, that means simulating an unavailable provider, checking that further payment calls stop, and confirming that customers can still use unrelated features. When the provider recovers, the test should verify that trial calls remain bounded and that another failure returns the application to its protected state.

The tests should also exercise the customer-visible outcomes: an unsent payment, an uncertain payment result, and a successful reconciliation. These are different situations even if all began with the same error message in a log. Contract and integration tests can help check different parts of that behaviour, but an interface contract alone will not establish that recovery works under load.

Operational reporting should make those distinctions visible as well. A low downstream error count might simply mean the breaker is rejecting most calls before they leave the application. Record rejected calls, time spent open, recovery outcomes, and the effect on completed orders. As with any dashboard, the useful measure is one that helps someone understand the condition of the service.

The pattern earns its place when the team can explain what causes traffic to stop, what happens to the user's work during that interruption, and how normal operation returns. That turns a dependency outage into a condition the application has been designed to handle.

A dependency failure becomes easier to manage when the surrounding system has already decided what it can safely do without it.
Julia Norton

© 2026 Julia Norton.