# Circuit Breaker Pattern in .NET

> A circuit breaker temporarily blocks calls after a dependency exceeds a configured failure threshold. This .NET guide configures Polly and HttpClient, shows where retries and timeouts belong, and handles an open circuit without inventing a successful result.

Published: 2026-10-27. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/circuit-breaker-pattern-dotnet

A **circuit breaker** temporarily rejects calls after a dependency's failure rate reaches a configured threshold.
In .NET, Polly tracks outcomes across calls, opens the circuit for a break duration, and then allows a trial call to test recovery.
Retries and timeouts complement the breaker but need their own limits and failure predicates.

## Why Circuit Breakers?

Imagine your service calls a payment API. The API goes down. Without a circuit breaker:

1. Every request waits for the payment API to respond
2. Outstanding calls retain memory and occupy connection capacity
3. Retrying callers increase pressure on the dependency
4. Latency and resource exhaustion can spread to your service

Asynchronous I/O releases its thread while waiting.
Thread pool exhaustion is a separate risk if you block on async calls with `.Result` or `.Wait()`.

A [**retry pattern**](https://milanjovanovic.tech/blog/retry-pattern-dotnet-polly) helps with transient failures, but when a service stays down, retrying adds load to an already struggling system.

The **circuit breaker** solves this by failing fast when it detects the downstream service is unhealthy.

## How It Works

The circuit breaker has three states:

**Closed (normal):** All calls pass through. The circuit breaker monitors success and failure rates.

**Open (tripped):** After too many failures, the circuit breaker opens. All calls are immediately rejected without attempting the downstream call.

**Half-Open (testing):** After the break duration, the next call becomes a trial call in Polly.
Other callers remain blocked while that trial runs; its outcome determines whether the breaker closes or reopens.

![Circuit breaker state machine: Closed trips to Open when the failure threshold is exceeded, Open moves to Half-Open after the break duration, and a trial call in Half-Open either closes the circuit on success or reopens it on failure](https://milanjovanovic.tech/blogs/articles/circuit-breaker-pattern-dotnet/circuit-breaker-states.png)

## Implementation With Polly

Create a .NET 10 web project with `dotnet new web -n CircuitDemo` and install the HTTP resilience package, which also brings in Polly's dependency-injection integration:

```bash
dotnet add package Microsoft.Extensions.Http.Resilience
```

Use these namespaces in `Program.cs`:

```csharp
using System.Net;
using System.Net.Http.Json;
using Microsoft.Extensions.Http.Resilience;
using Polly;
using Polly.CircuitBreaker;
using Polly.Registry;
using Polly.Retry;
using Polly.Timeout;
```

Register a shared Polly pipeline before `builder.Build()`:

```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResiliencePipeline("payment-api", pipeline =>
{
    pipeline.AddCircuitBreaker(new CircuitBreakerStrategyOptions
    {
        FailureRatio = 0.5,                             // Open after 50% failure rate
        SamplingDuration = TimeSpan.FromSeconds(30),     // Measure over 30-second windows
        MinimumThroughput = 10,                          // Need at least 10 calls to evaluate
        BreakDuration = TimeSpan.FromSeconds(30),        // Stay open for 30 seconds
        ShouldHandle = new PredicateBuilder()
            .Handle<HttpRequestException>(TransientHttp.IsFailure)
            .Handle<TimeoutRejectedException>()
    });
    pipeline.AddTimeout(TimeSpan.FromSeconds(5));
});
```

Use this predicate when an HTTP method such as `GetFromJsonAsync` converts status errors into exceptions:

```csharp
public static class TransientHttp
{
    public static bool IsFailure(HttpRequestException exception) =>
        exception.StatusCode is null or HttpStatusCode.RequestTimeout
            or HttpStatusCode.TooManyRequests ||
        (int)exception.StatusCode.Value >= 500;
}
```

The breaker records failures and rethrows them; it does not retry or substitute a result.
Resolve and reuse this pipeline through the registry so its state survives individual calls.

**Parameters explained:**

- `FailureRatio`: the percentage of failed calls that triggers the circuit breaker
- `SamplingDuration`: the time window over which failures are counted
- `MinimumThroughput`: minimum number of calls before the ratio is evaluated (prevents opening on a single failure)
- `BreakDuration`: how long the circuit stays open before moving to half-open

## Circuit Breaker for HTTP Clients

The most common use case is protecting HTTP calls to external APIs:

```csharp
builder.Services.AddHttpClient("PaymentApi", client =>
{
    client.BaseAddress = new Uri("https://payments.example.com");
})
.AddResilienceHandler("circuit-breaker", pipeline =>
{
    pipeline.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
    {
        FailureRatio = 0.5,
        SamplingDuration = TimeSpan.FromSeconds(30),
        MinimumThroughput = 10,
        BreakDuration = TimeSpan.FromSeconds(60)
    });
    pipeline.AddTimeout(TimeSpan.FromSeconds(5));
});
```

Or use the standard resilience handler that includes retry + circuit breaker + timeout:

```csharp
builder.Services.AddHttpClient("PaymentApi")
    .AddStandardResilienceHandler(options =>
        options.Retry.DisableForUnsafeHttpMethods());
```

Choose one registration for a given client.
The standard handler retries all HTTP methods by default, so the example disables retries for unsafe methods until their side effects have an explicit idempotency contract.
Its breaker observes HTTP responses, including 408, 429, and 5xx, without requiring `EnsureSuccessStatusCode()`.

I covered how to customize these defaults in [**overriding default HTTP resilience handlers in .NET**](https://milanjovanovic.tech/blog/overriding-default-http-resilience-handlers-in-dotnet).

## Combining Retry and Circuit Breaker

Strategies execute in the order you add them, first added outermost.
Put retry **outside** the circuit breaker when you want the breaker to count each attempt:

```csharp
builder.Services.AddResiliencePipeline("external-api", pipeline =>
{
    pipeline.AddTimeout(TimeSpan.FromSeconds(20));

    // Retry (outer) - handles transient failures with backoff
    pipeline.AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        BackoffType = DelayBackoffType.Exponential,
        Delay = TimeSpan.FromMilliseconds(500),
        UseJitter = true,
        ShouldHandle = new PredicateBuilder()
            .Handle<HttpRequestException>(TransientHttp.IsFailure)
            .Handle<TimeoutRejectedException>()
    });

    // Circuit breaker - stops calls when the service is down
    pipeline.AddCircuitBreaker(new CircuitBreakerStrategyOptions
    {
        FailureRatio = 0.5,
        SamplingDuration = TimeSpan.FromSeconds(30),
        MinimumThroughput = 10,
        BreakDuration = TimeSpan.FromSeconds(30),
        ShouldHandle = new PredicateBuilder()
            .Handle<HttpRequestException>(TransientHttp.IsFailure)
            .Handle<TimeoutRejectedException>()
    });

    // Timeout (innermost) - per-attempt timeout
    pipeline.AddTimeout(TimeSpan.FromSeconds(5));
});
```

The flow:

1. The retry strategy allows the original attempt plus up to three retries with backoff and jitter
2. Each attempt passes through the circuit breaker; an open circuit throws `BrokenCircuitException`, which this retry predicate excludes
3. Each attempt that reaches the network has a 5-second timeout
4. The outer timeout caps the entire operation, including backoff, at 20 seconds

This ordering is what .NET's own `AddStandardResilienceHandler` uses.
When the circuit opens, the current operation stops as soon as an attempt encounters it.
If retry goes inside the breaker, the breaker instead records one final outcome per logical operation.
That is a different measurement boundary, and can be appropriate when you want to count operations rather than individual attempts.

## Handling the Open Circuit

For a runnable read-only example, use the `payment-api` pipeline above and add this registration and endpoint before the type declarations in `Program.cs`:

```csharp
builder.Services.AddHttpClient("PaymentStatus", client =>
    client.BaseAddress = new Uri("https://payments.example.com"));

var app = builder.Build();
app.MapGet("/payments/{id:guid}", async (
    Guid id, IHttpClientFactory clients,
    ResiliencePipelineProvider<string> pipelines, CancellationToken ct) =>
{
    var pipeline = pipelines.GetPipeline("payment-api");
    var client = clients.CreateClient("PaymentStatus");
    try
    {
        var result = await pipeline.ExecuteAsync(async token =>
        {
            return await client.GetFromJsonAsync<PaymentStatus>(
                $"/api/payments/{id}", token)
                ?? throw new InvalidOperationException("Empty payment response.");
        }, ct);
        return Results.Ok(result);
    }
    catch (BrokenCircuitException)
    {
        return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
    }
});
app.Run();

public sealed record PaymentStatus(Guid Id, string Status);
```

This endpoint maps an open circuit to 503; use your normal exception handling for other upstream errors and timeouts.
A fallback may return cached data when the contract allows it.
Never claim that a payment was queued unless you have durably queued it, and never retry a payment POST without server-side idempotency.

## Listening for State Changes

Monitor state transitions when configuring the breaker.
For example, with an `ILogger` named `logger` available in the registration callback:

```csharp
pipeline.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
    FailureRatio = 0.5,
    SamplingDuration = TimeSpan.FromSeconds(30),
    MinimumThroughput = 10,
    BreakDuration = TimeSpan.FromSeconds(30),
    ShouldHandle = new PredicateBuilder()
        .Handle<HttpRequestException>(TransientHttp.IsFailure)
        .Handle<TimeoutRejectedException>(),
    OnOpened = args =>
    {
        logger.LogWarning(
            "Circuit opened for {Duration}. Reason: {Reason}",
            args.BreakDuration,
            args.Outcome.Exception?.Message);
        return ValueTask.CompletedTask;
    },
    OnClosed = _ =>
    {
        logger.LogInformation("Circuit closed - service recovered");
        return ValueTask.CompletedTask;
    },
    OnHalfOpened = _ =>
    {
        logger.LogInformation("Circuit half-opened - testing recovery");
        return ValueTask.CompletedTask;
    }
});
```

Use these events for **alerting** and dashboards.

## Circuit Breaker vs Retry

The two patterns answer different failure modes:

- **Retry** handles transient, momentary failures. It repeats the same call after a brief delay. It works well for a blip in connectivity, but blind retries can overwhelm a service that's already struggling.
- **Circuit breaker** handles prolonged outages. It stops calling the failing service entirely for a period. It helps most when the dependency stays down, at the risk of rejecting calls slightly too aggressively during recovery.

They complement each other, which is why the pipeline above uses both.
I've written a dedicated comparison in **retry vs circuit breaker** if you're deciding which to reach for first.

## Practical Configuration Tips

1. **Set `MinimumThroughput` high enough.** A single failed call shouldn't open the circuit. Use 10-20 as a starting point.

2. **Match `BreakDuration` to your recovery time.** If the downstream service typically recovers in 30 seconds, set the break to 30-60 seconds.

3. **Monitor your circuits.** Use OpenTelemetry or logs to track when circuits open and close. Frequent opens indicate a reliability problem.

4. **Use per-service circuit breakers.** Don't share one circuit breaker across all calls. A failing payment service shouldn't break your email service.

5. **Combine with health checks.** If a circuit opens frequently, the health check can mark the service as degraded.

## Summary

The circuit breaker pattern prevents cascading failures by failing fast when a downstream service is unhealthy.

Reuse a breaker per dependency, count only the failures relevant to that dependency, and monitor state transitions.

Start with [**the standard HTTP resilience handler**](https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience) for HTTP clients and configure which methods may be retried.
For the bigger picture of resilience in cloud systems, see [**building resilient cloud applications with .NET**](https://milanjovanovic.tech/blog/building-resilient-cloud-applications-with-dotnet).

## Frequently asked questions

### What problem does the circuit breaker pattern solve?

It temporarily rejects calls to a dependency with a high recent failure rate. That reduces outstanding requests and repeated downstream work during an outage. Asynchronous HTTP calls do not occupy a thread while waiting, but they still retain request state and consume other resources.

### What are the three states of a circuit breaker?

Closed (calls flow normally while failures are counted), Open (calls are rejected immediately after the failure threshold trips), and Half-Open (after the break duration, a trial call tests whether the dependency has recovered).

### Should retry go inside or outside the circuit breaker?

Place retry outside when the breaker should count each attempt, as .NET's standard resilience handler does. Exclude BrokenCircuitException from the retry predicate so an open circuit ends the operation immediately. An outer breaker instead measures the final outcome of each logical operation.

### What is the difference between retry and circuit breaker?

Retry handles brief transient failures by attempting the call again. A circuit breaker handles sustained outages by stopping calls entirely for a period. They complement each other and are typically combined in one pipeline.

### How do I add a circuit breaker to HttpClient in .NET?

Use Microsoft.Extensions.Http.Resilience: call AddStandardResilienceHandler on the HttpClient registration to get retry, circuit breaker, and timeouts preconfigured, or AddResilienceHandler to compose your own pipeline.
