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:
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:
builder.Services.AddHttpClient<WeatherClient>(client =>
{
client.BaseAddress = new Uri("https://api.weather.example.com/");
})
.AddStandardResilienceHandler();
This single line adds:
- Total request timeout — 30 seconds across all retry attempts
- Retry — up to 3 retries with exponential backoff and jitter
- Circuit breaker — opens after a failure ratio threshold
- Attempt timeout — 10 seconds per individual attempt
Custom Resilience Pipelines
When the defaults don't fit, build a custom pipeline:
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:
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:
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:
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:
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:
builder.Services.AddHttpClient<WeatherClient>()
.AddStandardResilienceHandler();
// Resilience events are emitted as metrics automatically
// View them via OpenTelemetry, dotnet-counters, or your APM tool
Key Takeaways
- Use
AddStandardResilienceHandler()as a starting point — its defaults are well-chosen for most HTTP scenarios. - Customise individual strategies when you know the failure characteristics of the downstream service.
- Order strategies carefully: total timeout → retry → circuit breaker → attempt timeout.
- Resilience pipelines work for any async operation, not just HTTP.
- Always add jitter to retry delays to avoid thundering herd problems.
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.