The Circuit Breaker Pattern in ASP.NET Core: When to Use It and How

Every distributed system eventually faces the same failure mode: one slow or unavailable dependency pulls the entire application down with it. Threads pile up waiting for responses that never come, connection pools exhaust, and what started as a payment gateway timing out ends in a full service outage. The circuit breaker pattern exists precisely to break this chain. Understanding when it genuinely helps โ and when it adds complexity without benefit โ is the decision most enterprise teams get wrong.
If you want production-ready implementations that go beyond the concepts covered here, the complete annotated source code with edge cases, structured logging wired to OpenTelemetry, and full test coverage is available on Patreon โ built for teams shipping real ASP.NET Core APIs, not toy examples.
Understanding the circuit breaker pattern is foundational to building resilient ASP.NET Core APIs. Chapter 10 of the Zero to Production course covers it as part of a broader resilience strategy โ alongside rate limiting, retry policies, and Polly's AddStandardResilienceHandler โ wired into a complete production API codebase.
What Problem Does the Circuit Breaker Actually Solve?
The pattern takes its name from electrical engineering. A household circuit breaker trips when current exceeds a safe threshold โ rather than letting the wiring overheat, it opens the circuit and stops the flow entirely. The software equivalent does the same for external calls.
In a microservices or integration-heavy ASP.NET Core API, every outbound call โ to a database, an external REST service, a message broker โ is a potential failure point. The naive approach is to retry failed calls. But retrying a service that is genuinely down compounds the problem: the caller burns threads, the downstream service receives a thundering herd of retries as soon as it starts to recover, and the cascade of failures spreads.
A circuit breaker short-circuits that loop. It monitors call outcomes and, once failures cross a threshold, stops allowing calls through entirely for a configured duration. The downstream service gets breathing room to recover, the caller fails fast with a known error instead of waiting for timeouts, and the rest of your system stays operational.
The Three States Every Team Needs to Understand
The circuit breaker is a state machine with three states. Getting the state transitions right โ and understanding what happens at each transition โ is where most implementations go wrong.
Closed is the normal operating state. All calls pass through. The circuit breaker tracks outcomes and counts failures. As long as failures stay below the configured threshold, the circuit stays closed.
Open is the protective state. The failure threshold has been crossed. All calls are immediately rejected with a BrokenCircuitException โ no actual call to the downstream service is made. The circuit stays open for a configured break duration.
Half-Open is the recovery probe state. After the break duration elapses, the circuit enters half-open and allows a limited number of probe calls through. If those calls succeed, the circuit resets to closed. If they fail, the circuit trips back to open and the timer resets.
The half-open state is critical and often under-configured. A circuit that probes with too many concurrent requests can overwhelm a service that is still fragile. Polly v8's CircuitBreakerStrategyOptions<T> lets you set MinimumThroughput and configure sampling windows โ a significant improvement over the v7 approach where you had to estimate everything manually.
How Polly v8 Changes the Circuit Breaker API
Most articles still show Polly v7 syntax โ HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(...). That API still works but Polly v8 introduced a fundamentally different model: ResiliencePipeline and ResiliencePipelineBuilder.
The key difference is that v8 uses a rate-based failure threshold rather than a simple count. Instead of "trip after 5 failures", you configure "trip when the failure rate exceeds 50% over the last 10 seconds with at least 5 calls sampled". This is far more meaningful for high-throughput production APIs where 5 failures in 10,000 calls is noise, but 50% failure over 30 seconds is a real incident.
new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10,
FailureRatio = 0.5,
BreakDuration = TimeSpan.FromSeconds(15),
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => (int)r.StatusCode >= 500)
}
This snippet illustrates the shape of the configuration โ the complete wiring with IHttpClientFactory, named pipelines, and telemetry hooks is where the real implementation lives.
The AddStandardResilienceHandler() extension on IHttpClientBuilder goes even further. It wires a full stack โ retry, circuit breaker, and timeout โ with sensible defaults in a single call. For teams that want opinionated defaults and don't need fine-grained per-service tuning, this is the right starting point.
When the Circuit Breaker Pattern Genuinely Fits
Not every ASP.NET Core application needs a circuit breaker. Applying it everywhere is one of the most common resilience anti-patterns โ it adds latency monitoring overhead, increases configuration surface, and can cause false trips that degrade availability in ways that are hard to debug.
Use it when:
You are calling external services you do not control โ payment gateways, identity providers, third-party APIs. These can go down without warning and for extended durations. A circuit breaker is the difference between your order service returning a graceful error and your entire request pipeline stalling.
You have fan-out patterns where a single incoming request triggers multiple downstream calls. One slow dependency cascading through all fan-out branches will multiply your thread consumption.
Your downstream service has predictable recovery behaviour โ it can handle the probe calls during the half-open window without retriggering failures. If recovery is inherently noisy, tune
MinimumThroughputto avoid thrashing.You are building multi-tenant SaaS where one tenant's integration being degraded must not affect other tenants' experience. Per-tenant or per-client circuit breaker instances (keyed by tenant ID) are the correct design here.
You want observability into degradation events without relying on manual log scanning. Polly v8's telemetry events (
OnCircuitOpened,OnCircuitClosed,OnCircuitHalfOpened) integrate natively with OpenTelemetry, giving you automatic metrics and trace spans.
When to Skip It
Do not use a circuit breaker when:
You are calling your own internal services over a local network with sub-millisecond latency and no shared failure domain. Retries with exponential backoff are cheaper and sufficient.
The operation is idempotent and fast-failing by design. If your Redis cache is unreachable, you should fall through to the database immediately โ a circuit breaker adds latency to a fast path that should not have it.
You are calling a database directly in most cases. Database connection pooling and command timeouts give you the same protection with less configuration overhead. Circuit breakers around DbContext calls produce confusing behaviour because EF Core manages its own retry and connection resilience.
The failure mode is not transient. A 400 Bad Request from a payment API is a logic error, not a transient fault. A circuit breaker that counts it as a failure will open on perfectly healthy services due to bad input.
Your team has not yet instrumented the application. Running circuit breakers without observability means you will never know when they trip in production โ and trips that you cannot see are worse than not having the pattern at all.
Circuit Breaker vs Retry vs Timeout: The Decision Matrix
These three patterns address the same class of problem but at different layers. They are complementary, not interchangeable.
| Concern | Retry | Circuit Breaker | Timeout |
|---|---|---|---|
| Transient network glitch | โ Best fit | โ Overkill | โ Wrong layer |
| Dependency sustained outage | โ Worsens it | โ Best fit | โ ๏ธ Partial |
| Slow dependency (not down) | โ No effect | โ May not trip | โ Best fit |
| Cascading failure prevention | โ Amplifies | โ Best fit | โ ๏ธ Partial |
| Fast-fail on known bad state | โ No | โ Best fit | โ No |
A well-designed resilience pipeline combines all three โ timeout wraps the individual call, retry handles transient glitches after the call completes, and circuit breaker trips when the overall failure rate indicates a sustained problem. AddStandardResilienceHandler() wires exactly this stack.
The important ordering rule: retry should never sit inside a circuit breaker's protected scope such that it masks failures from the circuit breaker's counter. With Polly v8 pipelines, the order of strategies in the builder matters โ outer strategies execute first on the call and last on the return path.
Anti-Patterns That Create More Problems Than They Solve
Sharing one circuit breaker across all clients. If five services share a single circuit breaker instance and one is degraded, the breaker trips for all five. Each named HttpClient โ or each key in a keyed pipeline โ should have its own circuit breaker instance.
Setting thresholds based on gut feel. The FailureRatio and MinimumThroughput values must come from your actual traffic and SLA data. A circuit breaker tuned in development will behave differently under production load. Instrument first, tune second.
Swallowing BrokenCircuitException silently. When the circuit is open, Polly throws BrokenCircuitException. This is a signal โ your application should respond with a meaningful fallback (cached response, degraded mode, clear user error), not a generic 500. The exception type tells you exactly what happened.
Opening the circuit on client errors (4xx). A correctly configured circuit breaker only counts failures that indicate service health issues โ 5xx responses and connection exceptions. A 401 Unauthorized or 422 Unprocessable Entity is your bug, not the downstream service's. The ShouldHandle predicate must filter these out.
Observability: Making Circuit Breaker State Visible
A circuit breaker that trips silently in production is a liability. Polly v8's telemetry integration with MeteringEnricher and TelemetryOptions emits meter events directly to OpenTelemetry. You get resilience.polly.circuit-state gauges and resilience.polly.strategy-events counters without writing any telemetry code yourself.
Add a health check that reflects circuit breaker state so your Kubernetes readiness probe knows when a critical dependency is in open state. An API that is healthy but cannot reach its payment provider should not receive traffic โ at least not for operations that require that provider. ASP.NET Core's health check system integrates cleanly with this model.
For teams already using Polly's full resilience pipeline capabilities in ASP.NET Core, the circuit breaker fits naturally into the pipeline builder alongside retry and timeout strategies.
FAQ: Circuit Breaker Pattern in ASP.NET Core
What is the circuit breaker pattern in ASP.NET Core?
The circuit breaker pattern is a resilience strategy that monitors outbound calls to external dependencies and automatically stops allowing those calls through when the failure rate crosses a threshold. It exists in three states โ closed (normal), open (fast-failing), and half-open (recovery probe) โ and prevents cascading failures in distributed systems.
When should I use a circuit breaker instead of just retrying?
Use retries for transient glitches โ a momentary network hiccup where the service is fundamentally healthy. Use a circuit breaker when the dependency is experiencing a sustained outage or degradation. Retrying a service that is genuinely down worsens the situation by increasing load on a recovering system. The two patterns are complementary: retries handle the individual glitch, circuit breakers handle the sustained incident.
Does Polly v8 change how I configure circuit breakers?
Yes, significantly. Polly v8 replaced the v7 HttpPolicyExtensions.CircuitBreakerAsync(...) API with ResiliencePipelineBuilder and CircuitBreakerStrategyOptions. The v8 approach uses a rate-based failure model (failure ratio over a sampling window) rather than a simple failure count, which is more accurate for production workloads. The AddStandardResilienceHandler() extension provides a pre-configured stack with sensible defaults.
Should I use circuit breakers around database calls?
Generally no. EF Core manages its own connection resilience and retry logic via EnableRetryOnFailure(). Adding a circuit breaker around DbContext calls creates complex interactions โ particularly with retry. For databases, configure command timeouts at the connection level and let EF Core's built-in resilience handle transient faults.
How do I prevent a circuit breaker from tripping on client errors like 400 or 401?
Configure the ShouldHandle predicate in CircuitBreakerStrategyOptions to only count 5xx responses and connection exceptions as failures. Client errors (4xx) are not indicators of service health โ they indicate a problem with the request payload or authentication, not the downstream service. Including them in the failure count will cause false trips on healthy services.
What happens to in-flight requests when a circuit breaker opens?
Requests that are already in-flight when the circuit opens will complete normally (or timeout, depending on your timeout configuration). The circuit breaker only affects new call attempts after the transition to open state. Those new calls receive an immediate BrokenCircuitException rather than waiting for the timeout to elapse.
How do I make circuit breaker state visible in production monitoring?
Polly v8 emits telemetry events natively through OpenTelemetry when you use ResiliencePipelineBuilder with telemetry enabled. You get circuit state metrics (closed, open, half-open) and event counters (OnCircuitOpened, OnCircuitClosed) automatically. Pair this with an ASP.NET Core health check that reflects circuit state to integrate with Kubernetes probes and alerting.
โ Prefer a one-time tip? Buy us a coffee โ every bit helps keep the content coming!






