# Circuit Breaker Pattern with Polly in .NET: Example and Settings

> Add retries and a circuit breaker to HttpClient in .NET with Microsoft.Extensions.Http.Resilience and Polly: default values, custom pipeline code, tuning and BrokenCircuitException fixes.

Source: https://www.epifive.com/blog/circuit-breaker-pattern-polly-dotnet-example · Published 2026-10-26 · EpiFive

## Key takeaways

- AddStandardResilienceHandler chains a rate limiter, total timeout, retry, circuit breaker and attempt timeout on an HttpClient with sensible defaults.
- The default circuit breaker opens at a 10 percent failure ratio, needs 100 requests in a 30-second window and stays open for 5 seconds.
- If minimum throughput is not reached within the sampling duration, the circuit never opens, so low-traffic services need smaller values.
- The standard handler retries every HTTP method by default, so disable retries for unsafe methods unless the API is idempotent.
- When the circuit is open, calls throw BrokenCircuitException, which your code should catch and turn into a fallback or a clear error.

To add a circuit breaker to outbound HTTP calls in .NET, install `Microsoft.Extensions.Http.Resilience` and call `AddStandardResilienceHandler()` on the `IHttpClientBuilder` returned by `AddHttpClient`. That adds retries, a circuit breaker and timeouts built on Polly with default values. For control over the thresholds, use `AddResilienceHandler` and configure `AddRetry` and `AddCircuitBreaker` yourself.

## Who this is for and what you will have

This guide is for .NET developers whose service calls other HTTP APIs and who want it to survive slow or failing dependencies without piling up requests. By the end you will have a typed `HttpClient` with a resilience pipeline, understand the documented defaults, be able to write a custom pipeline with tuned retry and circuit breaker settings, and know how to handle the exception thrown when the circuit opens.

## What a circuit breaker does

A retry helps when a failure is brief. If the dependency is down for minutes, retries add load to a service that is already struggling and make every caller wait. A circuit breaker watches recent calls and, once failures cross a threshold, blocks further calls for a short time so callers fail immediately.

Polly's circuit breaker has four states:

| State | Behaviour |
|---|---|
| Closed | Normal operation; calls execute |
| Open | Calls are blocked |
| Half-open | After the break duration, calls are allowed again to test recovery |
| Isolated | Held open manually; calls are blocked |

If the test calls succeed in the half-open state the circuit closes, and if they fail it opens again.

## Prerequisites

- A .NET project that uses dependency injection, such as ASP.NET Core or the .NET Generic Host.
- The NuGet package `Microsoft.Extensions.Http.Resilience`. The package relies on `Microsoft.Extensions.Resilience` and Polly.
- Calls made through `IHttpClientFactory` (`AddHttpClient`), because the resilience extensions attach to the `IHttpClientBuilder`. For a static or singleton `HttpClient` without a container, Microsoft points to its HttpClient guidelines on resilience with static clients.

Install the package:

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

## Step 1: Add the standard resilience handler

Register a typed client and chain the standard handler.

```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

IHttpClientBuilder httpClientBuilder = builder.Services.AddHttpClient<ExampleClient>(
    configureClient: static client =>
    {
        client.BaseAddress = new("https://api.contoso.com");
    });

httpClientBuilder.AddStandardResilienceHandler();
```

The documentation advises adding only one resilience handler to a client and not stacking handlers. If you need several, use `AddResilienceHandler` to customize the strategies.

The typed client itself is ordinary code:

```csharp
using System.Net.Http.Json;

internal sealed class ExampleClient(HttpClient client)
{
    public Task<Order[]?> GetOrdersAsync(CancellationToken ct) =>
        client.GetFromJsonAsync<Order[]>("/orders", ct);
}

public record class Order(int Id, string Status);
```

### Defaults of the standard handler

The standard handler chains five strategies, listed from outermost to innermost:

| Order | Strategy | What it does | Defaults |
|---|---|---|---|
| 1 | Rate limiter | Limits concurrent requests to the dependency | Queue 0, permit limit 1,000 |
| 2 | Total timeout | Caps the whole execution including retries | 30 seconds |
| 3 | Retry | Retries slow or transiently failing calls | 3 retries, exponential backoff, jitter on, 2-second delay |
| 4 | Circuit breaker | Blocks calls when too many failures or timeouts occur | Failure ratio 10%, minimum throughput 100, sampling duration 30 seconds, break duration 5 seconds |
| 5 | Attempt timeout | Limits each single attempt | 10 seconds |

The retry and circuit breaker strategies both handle HTTP 500 and above, HTTP 408 (request timeout) and HTTP 429 (too many requests), plus `HttpRequestException` and `TimeoutRejectedException`.

Because the order is outermost to innermost, the total timeout wraps the retries, and each retry attempt passes through the circuit breaker and gets its own attempt timeout.

## Step 2: Do not retry unsafe methods blindly

By default the standard handler retries every HTTP method. Microsoft warns this can be harmful: if a POST inserts a record, a retry can duplicate it. Two options are documented.

Disable specific methods:

```csharp
httpClientBuilder.AddStandardResilienceHandler(options =>
{
    options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Delete);
});
```

Or disable all methods the HTTP specification considers unsafe, which are POST, PATCH, PUT, DELETE and CONNECT:

```csharp
httpClientBuilder.AddStandardResilienceHandler(options =>
{
    options.Retry.DisableForUnsafeHttpMethods();
});
```

If the API supports idempotency keys, retrying writes becomes safe. See [Idempotent API design: keys, storage and retention](https://www.epifive.com/blog/idempotent-api-design-keys-storage-retention) and [Webhook retry and idempotency patterns](https://www.epifive.com/blog/webhook-retry-idempotency-patterns).

## Step 3: Write a custom pipeline

When the defaults do not fit, use `AddResilienceHandler` with a name and configure the strategies. This example is based on Microsoft's own sample.

```csharp
using System.Net;
using Microsoft.Extensions.Http.Resilience;
using Polly;

httpClientBuilder.AddResilienceHandler(
    "CustomPipeline",
    static builder =>
    {
        builder.AddRetry(new HttpRetryStrategyOptions
        {
            BackoffType = DelayBackoffType.Exponential,
            MaxRetryAttempts = 5,
            UseJitter = true
        });

        builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            SamplingDuration = TimeSpan.FromSeconds(10),
            FailureRatio = 0.2,
            MinimumThroughput = 3,
            ShouldHandle = static args =>
            {
                return ValueTask.FromResult(args is
                {
                    Outcome.Result.StatusCode:
                        HttpStatusCode.RequestTimeout or
                            HttpStatusCode.TooManyRequests
                });
            }
        });

        builder.AddTimeout(TimeSpan.FromSeconds(5));
    });
```

In this pipeline the circuit opens when at least 3 calls occur within 10 seconds and 20 percent of them fail, with failure defined as a 408 or 429 response. Note that the custom `ShouldHandle` here replaces the default, so a 500 response or an `HttpRequestException` would not count as a failure for this breaker. Choose the predicate deliberately.

A warning from the documentation: if you use both retry and timeout strategies and customize `ShouldHandle` on the retry, decide whether it should handle Polly's timeout exception. Polly throws `TimeoutRejectedException`, which inherits from `Exception`, not the standard `TimeoutException`.

Strategy order matters in a custom pipeline. Strategies added first are outermost. The standard handler puts the retry outside the circuit breaker, so each retry attempt is counted by the breaker and a retry that hits an open circuit fails fast.

## Circuit breaker settings explained

These are the Polly circuit breaker options and their defaults when you construct the strategy directly:

| Option | Default | Meaning |
|---|---|---|
| `ShouldHandle` | Any exception except `OperationCanceledException` | What counts as a failure |
| `FailureRatio` | 0.1 | Ratio of failures to total calls that opens the circuit |
| `MinimumThroughput` | 100 | Minimum calls in the sampling window before the ratio is evaluated |
| `SamplingDuration` | 30 seconds | Window over which the ratio is measured |
| `BreakDuration` | 5 seconds | How long the circuit stays open |
| `BreakDurationGenerator` | null | Computes the break duration at runtime and overrides `BreakDuration` if set |
| `ManualControl` | null | Lets code isolate or close the circuit |
| `StateProvider` | null | Exposes the current state |
| `OnClosed`, `OnOpened`, `OnHalfOpened` | null | Callbacks on state changes |

One setting causes the most confusion: if `MinimumThroughput` is not reached within `SamplingDuration`, the circuit will not break regardless of the failure ratio. With the defaults, a service making 20 calls a minute can see every call fail and never open the circuit. For low-traffic clients, lower `MinimumThroughput`, for example to 5 or 10, and test the result.

Note that the options for the standard handler are exposed on `HttpStandardResilienceOptions`, so you can adjust the circuit breaker without rewriting the whole pipeline:

```csharp
httpClientBuilder.AddStandardResilienceHandler(options =>
{
    options.CircuitBreaker.MinimumThroughput = 10;
    options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
});
```

## Step 4: Handle the broken circuit

When the circuit is open, the call does not run and Polly throws `BrokenCircuitException`, which exposes `RetryAfter` and `TelemetrySource`. A circuit held open manually throws `IsolatedCircuitException`. Catch these where you can give the user a sensible response.

```csharp
using Polly.CircuitBreaker;

try
{
    var orders = await client.GetOrdersAsync(ct);
    return Results.Ok(orders);
}
catch (BrokenCircuitException)
{
    // The dependency is failing; fail fast with a clear message or a cached value.
    return Results.Problem(
        statusCode: StatusCodes.Status503ServiceUnavailable,
        title: "The orders service is temporarily unavailable.");
}
```

Returning a cached value, a default or a queued task is usually better than an error when the data allows it. The circuit breaker only gives you speed and protection; the fallback is your decision.

## Monitoring state

Expose the current state with a `CircuitBreakerStateProvider` and use `OnOpened` and `OnClosed` callbacks to log transitions, so on-call engineers can see when a dependency was cut off.

```csharp
using Polly.CircuitBreaker;

var stateProvider = new CircuitBreakerStateProvider();

var options = new CircuitBreakerStrategyOptions
{
    StateProvider = stateProvider,
    OnOpened = args =>
    {
        Console.WriteLine("Circuit opened.");
        return default;
    }
};

// Later: stateProvider.CircuitState returns Closed, Open, HalfOpen or Isolated.
```

For manual control, `CircuitBreakerManualControl` provides `IsolateAsync()` and `CloseAsync()`. Isolating the circuit blocks all calls, which can be useful during a planned dependency outage.

## Hedging as an alternative

The package also offers `AddStandardHedgingHandler()`, which retries slow requests in parallel and uses a pool of circuit breakers so unhealthy endpoints are not hedged against. Its defaults include a 30-second total timeout, a hedging delay of 2 seconds and up to 10 attempts, with a per-endpoint rate limiter, circuit breaker and 10-second attempt timeout. It suits calls to multiple equivalent endpoints. If you only call one endpoint, the standard resilience handler is the simpler choice. You can switch between them with `RemoveAllResilienceHandlers()`.

## Verifying your configuration

1. Point the client at a test server you control that returns HTTP 500.
2. Send more requests than `MinimumThroughput` inside the sampling duration.
3. Confirm that after the ratio is crossed, calls start throwing `BrokenCircuitException` immediately rather than waiting for timeouts.
4. Wait for the break duration, send one request to a healthy server and confirm the circuit closes.
5. Check logs or metrics for the open and close transitions.

Test with a deliberately small `MinimumThroughput` and `SamplingDuration` in a test environment so the circuit opens quickly, and keep production values in configuration.

## Troubleshooting

### The circuit never opens even though every call fails

The sampling window did not see enough calls. With the defaults you need 100 requests in 30 seconds. Lower `MinimumThroughput`, and make sure `ShouldHandle` treats the failure you see as a failure. A custom predicate that only handles 408 and 429 will ignore 500 responses.

### Duplicate records after a transient error

The retry strategy resent a POST. Use `DisableFor` or `DisableForUnsafeHttpMethods`, or add idempotency keys to the API call.

### Requests time out at 10 seconds on a slow endpoint

The default attempt timeout is 10 seconds, and the default total timeout is 30 seconds. For slow calls such as report generation, raise the attempt and total timeouts through the options passed to `AddStandardResilienceHandler`, keeping the total larger than the attempt timeout multiplied by the number of attempts you need.

### Timeouts are not retried in a custom pipeline

Polly throws `TimeoutRejectedException`, not `TimeoutException`. If you wrote a `ShouldHandle` for the retry strategy, include that exception.

### gRPC client throws InvalidOperationException about ConfigureHttpClient

Documented known issue: `Grpc.Net.ClientFactory` version 2.63.0 or earlier does not work with the standard resilience or hedging handlers. Upgrade to 2.64.0 or later.

### Application Insights telemetry disappears

Documented known issue for Application Insights 2.22.0 or lower: registering resilience before Application Insights can stop telemetry. Update to 2.23.0 or later, or register Application Insights first.

### Retry on HTTP 429 hammers a rate-limited API

The standard handler treats 429 as a transient error. For APIs that return a `Retry-After` header, see [LLM API rate limits: retry and backoff in Python](https://www.epifive.com/blog/llm-api-rate-limits-retry-backoff-python) for the general pattern, and check the Polly retry documentation for how to honour the header in your pipeline.

## Checklist

- Install `Microsoft.Extensions.Http.Resilience` and use `AddHttpClient` so you can add the handler.
- Add exactly one resilience handler per client.
- Start with `AddStandardResilienceHandler()` and read its defaults before changing them.
- Disable retries for unsafe methods unless the endpoint is idempotent.
- Lower `MinimumThroughput` for low-traffic clients.
- Catch `BrokenCircuitException` and provide a fallback or clear error.
- Log circuit state changes and test the open and recovery behaviour before release.

## References

- Build resilient HTTP apps (Microsoft Learn): https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience
- Polly circuit breaker strategy: https://www.pollydocs.org/strategies/circuit-breaker.html
- Polly retry strategy: https://www.pollydocs.org/strategies/retry.html
- Microsoft.Extensions.Http.Resilience on NuGet: https://www.nuget.org/packages/Microsoft.Extensions.Http.Resilience

## Frequently asked questions

### What does a circuit breaker do in .NET?

A circuit breaker stops calling a dependency after too many recent failures, so callers fail fast instead of waiting on timeouts. After a break duration it lets a test call through in the half-open state, and closes again if that call succeeds. In Polly this is the circuit breaker strategy.

### What are the default circuit breaker settings in AddStandardResilienceHandler?

The documented defaults are a failure ratio of 10 percent, a minimum throughput of 100 requests, a sampling duration of 30 seconds and a break duration of 5 seconds. The circuit only opens if at least 100 requests occur inside the 30-second window and at least 10 percent of them fail.

### Should I retry POST requests with the standard resilience handler?

Be careful. By default the standard handler retries all HTTP methods, which can duplicate data when a POST creates a record. Use DisableFor with the methods you want excluded, or DisableForUnsafeHttpMethods to skip POST, PATCH, PUT, DELETE and CONNECT, unless the endpoint is idempotent.

### Which package do I install for Polly with HttpClientFactory?

Install Microsoft.Extensions.Http.Resilience with dotnet add package. It relies on Microsoft.Extensions.Resilience and Polly and adds resilience extension methods to the IHttpClientBuilder returned by AddHttpClient.

