.NET 10Polly.Core 8.8.0Microsoft.Extensions.Http.Resilience 10.11.0

Circuit Breaker in C# .NET 10 — Interactive Guide

A circuit breaker stops your app from hammering a dependency that is already failing. It fails fast, gives the dependency room to recover, and probes carefully before letting traffic back. This guide takes you from zero to production: the states, the exact math Polly uses, a live simulator you can break, a playground that writes the C# for you, and twelve real-world scenarios.

Your app HttpClient CLOSED OPEN HALF-OPEN Circuit breaker Payments remote API fail fast · let it recover · probe · resume

01 · The problem

Why circuit breakers exist

In a distributed system your code calls other things over the network: payment gateways, databases, caches, other microservices, AI APIs. Most failures are transient — a dropped packet, a brief overload — and a quick retry fixes them.

Some failures last longer: a crashed service, a locked database, an expired certificate, a regional outage. Retrying those just adds load to something that is already on fire. Worse, each waiting call holds a thread, a socket, a DB connection and memory. When enough calls are stuck, your service runs out of resources and starts failing too — even for features that never touch the broken dependency. That chain reaction is a cascading failure.

A circuit breaker sits in front of the dependency like a proxy. It watches recent outcomes; when too many fail it opens and rejects calls instantly for a while, then lets a single probe through to see if things recovered.

Analogy: the breaker in your home's fuse box. When a short circuit draws too much current, it trips so the wiring doesn't catch fire. You fix the fault, flip it back, and power returns. Software breakers flip themselves back — carefully.

Thread-pool view of an outage

One request arrives every tick. From tick 12 the dependency hangs until a 16-tick timeout. Each square is a worker thread.

Without a circuit breaker

With a circuit breaker

■ working · ■ stuck waiting on the broken dependency · empty = free

Circuit breaker vs. its neighbours

Polly groups strategies into reactive (react to a failure that already happened) and proactive (prevent one). A circuit breaker is reactive. It is not a retry: it does not retry anything and it re-throws the original failures while closed. You combine them.

StrategyTypeQuestion it answersTypical use
RetryReactiveWas that a blip? Try again.Short network glitches, 503 during deploys
Circuit breakerReactiveIs it broken? Stop calling for a while.Outages lasting seconds to minutes
TimeoutProactiveIs it taking too long? Give up.Turning hangs into countable failures
FallbackReactiveIt failed — what do we return instead?Cached data, default value, queue for later
Rate limiter (bulkhead)ProactiveAre we sending too much at once?Quotas, protecting a fragile dependency
HedgingReactiveSlow? Fire a parallel attempt and take the first.Latency-critical reads across replicas
Key idea: retry says “it will probably work next time”; a circuit breaker says “it probably won't work for a while”. Use retry for blips, the breaker for outages, and make retries stop when the breaker is open.

02 · State machine

The four states

Polly's breaker is a small state machine. Three states are automatic; the fourth, Isolated, is a manual override. Pick a state to see what happens to calls, what is recorded, and which callback and telemetry event fire.

CLOSED OPEN HALF-OPEN ISOLATED 1 2 3 4 5 6
  1. Closed → Open: in the sampling window, failures ÷ calls ≥ FailureRatio and calls ≥ MinimumThroughput.
  2. Open → Half-Open: BreakDuration has elapsed and a new call arrives (transitions are lazy).
  3. Half-Open → Closed: the probe call succeeds; statistics reset.
  4. Half-Open → Open: the probe fails; a new break starts.
  5. Any → Isolated: ManualControl.IsolateAsync().
  6. Isolated → Closed: ManualControl.CloseAsync().

03 · The decision rule

How Polly decides to break

Polly v8 has one circuit breaker, the “advanced” ratio-based one. Four options control it:

SamplingDuration

The rolling time window over which outcomes are counted. Default 30 s. Old outcomes slide out continuously (Polly keeps the window as small buckets).

MinimumThroughput

How many calls must be in the window before the ratio is even considered. Default 100. Below it, the circuit never breaks — even at 100% failures.

FailureRatio

The fraction of handled failures that trips the breaker. Default 0.1 (10%). Polly breaks when the ratio reaches the threshold.

BreakDuration

How long the circuit stays Open before the next call may probe. Default 5 s. Or compute it with BreakDurationGenerator (wins if both are set).

The rule: after each handled failure, if calls in window ≥ MinimumThroughput and failures ÷ calls ≥ FailureRatio → the circuit opens. What counts as a failure is decided by ShouldHandle (default: every exception except OperationCanceledException). Anything not handled counts as a success.

Try the rule

Things beginners miss

  • A handled result is still returned. If you handle HttpResponseMessage with status 500, the breaker counts it but your code still receives the 500 while the circuit is closed. The breaker re-throws / returns every outcome; it only blocks new calls once open.
  • Open means “not executed”. While open, your delegate is never invoked; Polly throws BrokenCircuitException (with RetryAfter).
  • Transitions are lazy. The breaker is not a background timer. It moves to Half-Open only when a call arrives after the break.
  • One probe at a time. In Half-Open Polly lets a single call through; other calls arriving during the probe are still rejected.
  • State lives in the pipeline instance. Build the pipeline once and reuse it (singleton or DI). A new pipeline per request means a new, always-closed breaker.
  • State is per process. Ten pods have ten independent breakers. That's normal and usually desirable.

04 · Hands on

Live state simulator

This runs the same algorithm as Polly's breaker. Make the service fail, slow it down past the 1 s attempt timeout, or isolate it by hand, and watch the state, the sampling window and the event log. Values are demo-friendly; Polly's defaults are 10% · 100 calls · 30 s · 5 s.

requestsuccessfailurerejected (BrokenCircuitException)rejected → fallback
Downstream service
CircuitBreakerStrategyOptions
Your code

ManualControl:

Circuit stateClosed

Failure ratio in window 0% / 50%

Throughput in window 0 / 10

What the next call gets

Executes normally.
Sent0
Succeeded0
Failed (reached service)0
Short-circuited0

Event log (Polly telemetry names)

    05 · Tune it

    Config playground

    Choose a traffic level and a failure story, then tune the four knobs. The chart replays 90 seconds instantly so you can compare settings, spot breakers that can never open, and copy ready-to-use C# for Polly or the HttpClient standard handler.

    succeededfailedshort-circuitedwindow failure ratiothreshold

      06 · Library deep dive

      Polly v8 — the ResiliencePipeline API

      Polly v8 (current release 8.8.0) replaced v7's policies with resilience pipelines. You build a pipeline from strategies, keep it, and execute delegates through it. Use this directly for anything that isn't an HttpClient call: databases, Redis, gRPC, SDKs, message brokers, file shares.

      1 · Your first breaker

      Create the pipeline once (it holds the circuit state), then run calls through it. Catch BrokenCircuitException to react to an open circuit.

      2 · Defaults you get with new CircuitBreakerStrategyOptions()

      PropertyDefaultMeaning
      ShouldHandleAny exception except OperationCanceledExceptionWhich exceptions/results count as failures
      FailureRatio0.110% of sampled calls failing trips the circuit
      MinimumThroughput100Calls needed in the window before the ratio matters
      SamplingDuration30 sRolling window for the ratio
      BreakDuration5 sFixed time Open before probing
      BreakDurationGeneratornullCompute the break at runtime (overrides BreakDuration)
      ManualControlnullIsolate / close the circuit from code
      StateProvidernullRead the current state (health checks, dashboards)
      OnOpened / OnClosed / OnHalfOpenednullCallbacks on state transitions

      3 · Handling results, not just exceptions

      Use the generic options to treat certain results (like HTTP 5xx) as failures. Remember: the breaker counts them, but your code still receives them while the circuit is closed.

      4 · Callbacks for logging and alerts

      Polly also emits telemetry for every transition (via Polly.Extensions), so you rarely need to log by hand:

      Event nameSeverityWhen
      OnCircuitOpenedErrorJust before OnOpened runs
      OnCircuitHalfOpenedWarningJust before OnHalfOpened runs (result is always empty)
      OnCircuitClosedInformationJust before OnClosed runs

      5 · Dynamic break duration

      A fixed 5 s break can be too eager for a service that needs minutes to recover. BreakDurationGenerator receives runtime information such as FailureCount, FailureRate and HalfOpenAttempts, so you can back off exponentially between probes.

      6 · Reading and controlling state

      CircuitBreakerStateProvider reads the state; CircuitBreakerManualControl forces it. Isolated calls throw IsolatedCircuitException, which derives from BrokenCircuitException.

      Don't check StateProvider.CircuitState and skip the call when it's Open. The breaker only moves to Half-Open when a call is executed, so a guard like that keeps the circuit open forever. Execute and catch instead — or use ExecuteOutcomeAsync to avoid exceptions.

      7 · No-throw execution on hot paths

      ExecuteOutcomeAsync captures exceptions in an Outcome<T> instead of throwing — cheaper when the circuit is open and thousands of calls per second are rejected.

      8 · Strategy order: who wraps whom

      Strategies execute from the first added (outermost) to the last added (innermost). The usual order around a breaker:

      Fallback — outermost: catches real failures and BrokenCircuitException
      Total timeout — caps the whole operation, retries included
      Retry — outside the breaker, so every attempt is counted and retries stop when it opens
      Circuit breaker — sees each individual attempt's outcome
      Attempt timeout — innermost: turns a hang into a countable TimeoutRejectedException

      9 · Dependency injection with AddResiliencePipeline

      With Polly.Extensions, register pipelines by key and resolve them from ResiliencePipelineProvider<string>. The provider caches the instance, so the circuit state is shared app-wide — exactly what you want.

      10 · Passing data between strategies with ResilienceContext

      Polly's docs recommend sharing information through the context rather than coupling strategies via a state provider. Here the breaker publishes its break duration and the retry waits that long:

      11 · Unit testing without waiting

      Pipelines accept a TimeProvider. With FakeTimeProvider you can jump past the break duration instantly. Polly.Testing lets you inspect a built pipeline's strategies and options.

      12 · Coming from Polly v7?

      Polly v7Polly v8
      Policy.Handle<T>().AdvancedCircuitBreakerAsync(0.5, 10s, 8, 30s)AddCircuitBreaker(new() { FailureRatio = 0.5, SamplingDuration = 10s, MinimumThroughput = 8, BreakDuration = 30s })
      CircuitBreakerAsync(5, 30s) (consecutive failures)No direct equivalent. Approximate with FailureRatio = 1.0, MinimumThroughput = 5 and a short SamplingDuration
      onBreak / onReset / onHalfOpenOnOpened / OnClosed / OnHalfOpened
      policy.CircuitStateCircuitBreakerStateProvider.CircuitState
      policy.Isolate() / policy.Reset()CircuitBreakerManualControl.IsolateAsync() / CloseAsync()
      Microsoft.Extensions.Http.Polly, Polly.Extensions.HttpDeprecated → Microsoft.Extensions.Http.Resilience

      07 · HttpClient integration

      Microsoft.Extensions.Http.Resilience

      Microsoft's official wrapper around Polly v8 for IHttpClientFactory (current release 10.11.0, works with .NET 10). It plugs a resilience pipeline into the HttpClient handler chain, so every request goes through it automatically — no ExecuteAsync calls in your code. It also wires up DI, options binding, logging and metrics.

      1 · The standard resilience handler

      One line gives you five strategies, chained from outermost to innermost. Step through a failing request to see how they cooperate:

      1 · Rate limiterqueue 0 · permits 1,000
      2 · Total timeout30 s
      3 · Retry3 × exponential + jitter · 2 s
      4 · Circuit breaker10% · 100 calls · 30 s · break 5 s
      5 · Attempt timeout10 s
      HttpClientHandler → network → remote API

      Step 0 / 9

      Press Next to send a GET request while the remote API is returning 503.

      2 · What the standard handler treats as failure

      Both the retry and the circuit breaker handle:

      A 400, 401, 403 or 404 is your problem, not the server's health — it is not counted. Retries are enabled for all HTTP methods by default; turn them off for unsafe ones.

      3 · Customising the circuit breaker in the standard handler

      Validation rule: the standard handler requires the circuit breaker's SamplingDuration to be at least double the attempt timeout (defaults: 30 s vs 10 s). Raise the attempt timeout to 20 s and you must raise sampling to ≥ 40 s, or startup validation fails.

      4 · Settings from appsettings.json

      5 · One breaker per host: SelectPipelineByAuthority

      By default a named or typed client gets one pipeline — one circuit — no matter which host you call. If a single client talks to many independent hosts, one bad host would open the circuit for all of them. Select pipelines by authority (scheme + host + port) to get a breaker per host.

      6 · Fully custom pipeline with AddResilienceHandler

      Need a different order, a fallback, or your own strategies? Build the pipeline yourself. HttpCircuitBreakerStrategyOptions already handles the transient HTTP failures listed above.

      7 · App-wide defaults, with exceptions

      Add only one resilience handler per client — stacking handlers multiplies retries and breakers. Use RemoveAllResilienceHandlers to replace a default.

      8 · Standard hedging handler: a breaker per endpoint

      Hedging fires extra attempts against other endpoints when one is slow. It keeps a pool of circuit breakers, one per endpoint (by authority), so it never hedges against an endpoint whose circuit is open. Its pipeline: total timeout 30 s → hedging (up to 10 attempts, 2 s delay) → per-endpoint rate limiter → per-endpoint circuit breaker → per-endpoint attempt timeout 10 s.

      9 · Handling an open circuit in your client and API

      When the circuit is open, HttpClient calls throw BrokenCircuitException — not HttpRequestException. Catch it where you can degrade, and map it to 503 + Retry-After for your own callers.

      10 · Dynamic reload and known issues

      08 · In production

      Real-world scenario explorer

      Twelve situations teams actually hit. Pick one, then step through the timeline to see the circuit state, what the caller does, and the C# that implements it.

      How to build it in C#

      09 · Judgement

      When to use it — and when not to

      Use it

      • Calls cross a process or network boundary: HTTP/gRPC services, databases, caches, brokers, third-party SaaS, AI APIs.
      • The dependency can be down or degraded for seconds to minutes, not just milliseconds.
      • Waiting calls consume scarce resources (threads, sockets, connection pools, memory) and could cascade.
      • You want to protect latency SLOs from a slow dependency — slow is the new down.
      • You have something useful to do instead: cached data, a default, a queue, an alternative provider, a clear error.
      • Each call costs money or quota (paid APIs, LLM tokens, rate-limited SaaS).
      • You need a real-time failure signal to route traffic, e.g. between regions or providers.

      Skip it

      • In-process work: in-memory data structures, pure computation, local file parsing. A breaker only adds overhead.
      • As a substitute for business-logic error handling: validation errors, 400/404, “insufficient funds” are not outages.
      • Simple retries are enough and the dependency is built to absorb them.
      • Waiting for a reset would cause unacceptable delays and there is no alternative path anyway.
      • Message- or event-driven flows where queue retries and dead-letter queues already isolate failures.
      • The platform already does it for this call: a service mesh (Envoy/Istio outlier detection), an API gateway policy, or load-balancer health probes. Avoid double layers with conflicting thresholds.
      • Traffic is so low that ratios are meaningless — unless you tune MinimumThroughput and SamplingDuration for it.

      Quick decision helper

      10 · Experience

      Pitfalls & best practices

      Counting the wrong failures

      Handling every exception, including 400/404 and validation errors — a single buggy client request pattern opens the circuit.

      Handle only transient, server-side signals: 5xx, 408, 429, HttpRequestException, TimeoutRejectedException, driver-specific connection errors.

      A new pipeline per call

      Building ResiliencePipeline inside the method or a transient service — each call gets a fresh, closed breaker that never trips.

      Build once: a static field, a singleton, AddResiliencePipeline or AddResilienceHandler.

      One breaker for many dependencies

      Sharing one breaker across independent hosts, shards or tenants — one bad apple blocks them all.

      One breaker per independent dependency: separate clients/keys, or SelectPipelineByAuthority().

      Timeouts that are too long

      Relying on HttpClient.Timeout's 100 s default — the breaker sees nothing until calls finally fail, while threads pile up.

      Add an attempt timeout inside the breaker that matches the dependency's normal p99 latency plus headroom.

      Retry in the wrong place

      Retry inside the breaker, or a retry that blindly retries BrokenCircuitException with tiny delays.

      Retry outside the breaker; don't retry BrokenCircuitException, or wait at least its RetryAfter.

      Not handling the open state

      Letting BrokenCircuitException bubble up as a generic 500.

      Catch it where you can degrade; return 503 with a Retry-After header otherwise.

      Guarding with the state provider

      if (state != Open) Execute(...) — the circuit can never reach Half-Open.

      Always execute through the pipeline; use ExecuteOutcomeAsync if exceptions are too costly.

      Thresholds that can't trigger

      Defaults (100 calls / 30 s) on an endpoint that gets 10 calls a minute.

      Make sure rps × SamplingDuration comfortably exceeds MinimumThroughput. Test it in the playground.

      Retrying non-idempotent writes

      The standard handler retries POST by default — a timed-out payment may be charged twice.

      options.Retry.DisableForUnsafeHttpMethods(), idempotency keys, an outbox.

      Stacking handlers

      AddStandardResilienceHandler() on top of app defaults plus your own handler — retries multiply (3 × 3 × 3).

      One handler per client; RemoveAllResilienceHandlers() before adding a custom one.

      Flying blind

      No alert when a circuit opens; nobody notices until users complain.

      Export the Polly meter to OpenTelemetry, alert on OnCircuitOpened, show state on a dashboard.

      Breaking your own liveness

      Failing the liveness probe when a downstream circuit is open — Kubernetes restarts healthy pods.

      Report Degraded on readiness/health; never kill the process for someone else's outage.

      Testing with real time

      Thread.Sleep(30_000) in unit tests to wait out a break.

      FakeTimeProvider + ResiliencePipelineBuilder.TimeProvider; chaos/fault injection in staging.

      Forgetting where the load goes

      Falling back from a dead cache straight to the database at full traffic.

      Protect the fallback path too (rate limiter, concurrency limit, smaller page sizes).

      11 · Reference

      Glossary & cheat sheet

      SettingPolly CircuitBreakerStrategyOptionsStandard HTTP handlerStarting point to tune from
      FailureRatio0.10.10.1–0.5 depending on noise
      MinimumThroughput100100≈ 20–50% of calls per window
      SamplingDuration30 s30 s (≥ 2 × attempt timeout)Long enough for a stable ratio
      BreakDuration5 s5 sTypical recovery time; grow with a generator
      HandlesAny exception except cancellation5xx, 408, 429, HttpRequestException, TimeoutRejectedExceptionOnly transient, server-side failures
      Exception when openBrokenCircuitException (IsolatedCircuitException when isolated) with RetryAfter and TelemetrySourceCatch, degrade, or return 503
      Transient fault
      A short-lived failure that fixes itself (packet loss, brief overload). Retry territory.
      Cascading failure
      One failing component exhausts resources in its callers, which then fail their callers.
      Fail fast
      Reject immediately instead of waiting for a call that will probably fail.
      Short-circuit
      A call the open breaker rejected without executing your delegate.
      Sampling window
      The rolling SamplingDuration over which calls and failures are counted.
      Throughput
      Number of calls in the window. Must reach MinimumThroughput for the ratio to count.
      Break / trip
      Closed → Open. “The circuit broke.”
      Probe
      The single trial call allowed in Half-Open to test recovery.
      ShouldHandle
      Predicate deciding which exceptions/results are failures. Built with PredicateBuilder.
      RetryAfter
      Property on BrokenCircuitException: at least how long the circuit stays open.
      ResiliencePipeline
      Polly v8's composable chain of strategies; holds the breaker state.
      ResilienceContext
      Per-execution bag (cancellation token, operation key, properties) shared by strategies.
      Fallback
      What you return instead of failing: cache, default, alternative provider.
      Bulkhead / rate limiter
      Caps concurrency so one dependency can't consume every resource.
      Hedging
      Parallel attempts against alternatives when the first is slow; uses per-endpoint breakers in the standard hedging handler.
      Idempotent
      Safe to repeat with the same effect (GET, PUT with keys). Retry only these automatically.
      Authority
      Scheme + host + port of a URL. SelectPipelineByAuthority keys breakers by it.
      Graceful degradation
      Keeping the core experience working while a non-critical feature is off.
      Remember the whole idea in one line: count real failures over a window, stop calling when too many fail, wait, test with one call, resume — and always have a plan for the moment the circuit is open.