Architecture

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.

By EpiFive Editorial TeamReviewed/updated 10 min readHow we check articles
On this page

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:

StateBehaviour
ClosedNormal operation; calls execute
OpenCalls are blocked
Half-openAfter the break duration, calls are allowed again to test recovery
IsolatedHeld 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:

dotnet add package Microsoft.Extensions.Http.Resilience

Step 1: Add the standard resilience handler

Register a typed client and chain the standard handler.

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:

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:

OrderStrategyWhat it doesDefaults
1Rate limiterLimits concurrent requests to the dependencyQueue 0, permit limit 1,000
2Total timeoutCaps the whole execution including retries30 seconds
3RetryRetries slow or transiently failing calls3 retries, exponential backoff, jitter on, 2-second delay
4Circuit breakerBlocks calls when too many failures or timeouts occurFailure ratio 10%, minimum throughput 100, sampling duration 30 seconds, break duration 5 seconds
5Attempt timeoutLimits each single attempt10 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:

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:

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

If the API supports idempotency keys, retrying writes becomes safe. See Idempotent API design: keys, storage and retention and Webhook retry and 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.

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:

OptionDefaultMeaning
ShouldHandleAny exception except OperationCanceledExceptionWhat counts as a failure
FailureRatio0.1Ratio of failures to total calls that opens the circuit
MinimumThroughput100Minimum calls in the sampling window before the ratio is evaluated
SamplingDuration30 secondsWindow over which the ratio is measured
BreakDuration5 secondsHow long the circuit stays open
BreakDurationGeneratornullComputes the break duration at runtime and overrides BreakDuration if set
ManualControlnullLets code isolate or close the circuit
StateProvidernullExposes the current state
OnClosed, OnOpened, OnHalfOpenednullCallbacks 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:

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.

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.

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 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

Questions people ask

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.

Polly.NETHttpClientResilience
  1. Migrate Azure Functions from In-Process to the Isolated Worker Model

    In-process .NET function apps lose support on November 10, 2026. Find affected apps, convert the project, and cut over to the isolated worker with a slot swap.

    Architecture13 min read
  2. Azure Service Bus SBMP Retirement: Migrate to Azure.Messaging.ServiceBus

    SBMP and the WindowsAzure.ServiceBus and Microsoft.Azure.ServiceBus libraries reached end of support on 30 September 2026. Find affected code, switch to AMQP and move to Azure.Messaging.ServiceBus.

    Architecture10 min read
  3. Monolith vs Microservices for a Small Team: A Decision Guide

    Decide whether a modular monolith is enough or when to split into services, using the Azure Architecture Center's benefits, challenges and domain-analysis guidance.

    Architecture13 min read
  4. Azure Well-Architected Framework: Run a Review with the Five Pillars

    Run an Azure Well-Architected Review on a workload: scope it, work through the five pillars with checklists, weigh tradeoffs, use Azure Advisor and turn findings into a prioritised backlog.

    Architecture8 min read
  5. Caching Strategies: Cache-Aside, Write-Through and Invalidation with Redis

    Choose between cache-aside and write-through caching with Redis. Covers TTLs, invalidation order, eviction policies, stale reads, hit-ratio monitoring and Redis commands you can run today.

    Architecture9 min read
  6. ERP to Ecommerce Sync Patterns: Polling, Webhooks and Event Queues

    Compare polling, webhooks and queue-based sync for connecting an ERP to an online store, using Business Central webhooks as the worked example, with reconciliation, ordering and duplicate handling.

    Architecture8 min read