Polly Resilience Pipelines in ASP.NET Core

Network calls fail. Services go down, connections time out, and rate limits get hit. Polly v8 — shipped as Microsoft.Extensions.Resilience and Microsoft.Extensions.Http.Resilience — provides a composable way to handle these failures in ASP.NET Core applications.

The Polly v8 Model

Polly v8 replaced the older "policy" model with resilience pipelines. A pipeline is a chain of strategies that wrap around an operation. Each strategy handles a specific failure mode: retries, circuit breaking, timeouts, or rate limiting.

Install the packages:

terminal
dotnet add package Microsoft.Extensions.Http.Resilience

Adding Resilience to HttpClient

The simplest approach uses the built-in standard resilience handler, which bundles retry, circuit breaker, and timeout strategies with sensible defaults:

Program.cs
builder.Services.AddHttpClient<WeatherClient>(client =>
{
    client.BaseAddress = new Uri("https://api.weather.example.com/");
})
.AddStandardResilienceHandler();

This single line adds:

Custom Resilience Pipelines

When the defaults don't fit, build a custom pipeline:

Example.cs
builder.Services.AddHttpClient<PaymentClient>(client =>
{
    client.BaseAddress = new Uri("https://payments.example.com/");
})
.AddResilienceHandler("payments", builder =>
{
    builder.AddRetry(new HttpRetryStrategyOptions
    {
        MaxRetryAttempts = 2,
        Delay = TimeSpan.FromMilliseconds(500),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true,
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .Handle<HttpRequestException>()
            .HandleResult(r => r.StatusCode == HttpStatusCode.TooManyRequests
                            || r.StatusCode >= HttpStatusCode.InternalServerError)
    });

    builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
    {
        SamplingDuration = TimeSpan.FromSeconds(30),
        FailureRatio = 0.5,
        MinimumThroughput = 10,
        BreakDuration = TimeSpan.FromSeconds(15)
    });

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

Strategy ordering matters. Strategies execute from outer to inner — the first added is the outermost. A typical order is: total timeout, retry, circuit breaker, attempt timeout.

Resilience Pipelines Without HttpClient

You can use resilience pipelines for any operation, not just HTTP calls:

Example.cs
builder.Services.AddResiliencePipeline("database", builder =>
{
    builder.AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromMilliseconds(200),
        BackoffType = DelayBackoffType.Exponential
    });

    builder.AddTimeout(TimeSpan.FromSeconds(10));
});

Inject and use the pipeline:

ProductRepository.cs
public class ProductRepository
{
    private readonly ResiliencePipeline _pipeline;
    private readonly AppDbContext _dbContext;

    public ProductRepository(
        [FromKeyedServices("database")] ResiliencePipeline pipeline,
        AppDbContext dbContext)
    {
        _pipeline = pipeline;
        _dbContext = dbContext;
    }

    public async Task<Product?> GetByIdAsync(int id, CancellationToken cancellationToken)
    {
        return await _pipeline.ExecuteAsync(
            async token => await _dbContext.Products.FindAsync([id], token),
            cancellationToken);
    }
}

Handling Retry-After Headers

When an API returns 429 Too Many Requests with a Retry-After header, Polly can respect it:

Example.cs
builder.AddRetry(new HttpRetryStrategyOptions
{
    MaxRetryAttempts = 3,
    DelayGenerator = args =>
    {
        if (args.Outcome.Result?.Headers.RetryAfter is { } retryAfter)
        {
            var delay = retryAfter.Delta ?? TimeSpan.FromSeconds(1);
            return new ValueTask<TimeSpan?>(delay);
        }

        return new ValueTask<TimeSpan?>(TimeSpan.FromSeconds(Math.Pow(2, args.AttemptNumber)));
    }
});

Hedging

Hedging sends a parallel request if the first one takes too long. This is useful for latency-sensitive scenarios:

Example.cs
builder.AddHedging(new HttpHedgingStrategyOptions
{
    MaxHedgedAttempts = 2,
    Delay = TimeSpan.FromMilliseconds(500)
});

If the original request hasn't completed after 500ms, a second request is sent. The first successful response wins.

Observability

Polly v8 integrates with System.Diagnostics.Metrics. Add telemetry to see retry counts, circuit breaker state changes, and timeout events:

Example.cs
builder.Services.AddHttpClient<WeatherClient>()
    .AddStandardResilienceHandler();

// Resilience events are emitted as metrics automatically
// View them via OpenTelemetry, dotnet-counters, or your APM tool

Key Takeaways

Polly v8's integration with the .NET ecosystem makes resilience a configuration concern rather than a coding burden. Start with the standard handler, measure your failure modes, and tune from there.