Last Updated on May 25, 2025 by Aram
In the evolving world of cloud-native and distributed systems, resilience is no longer optional, it’s essential.
This article explores how developers can leverage the power of Polly via the new extensions to build resilience in ASP.NET Core
Starting from .NET 8 and built on top of Polly (v8), Microsoft.Extensions.Resilience and Microsoft.Extensions.Http.Resilience packages were added to allow you easily implement resilience mechanisms on HttpClient.
Check this document to learn more about Polly and the 6 strategies to build resilience in ASP.NET Core apps.
Introduction to Polly
Polly is a distinguished library that helps you build robust applications by enabling you to handle the transient faults
Transient faults are temporary issues or problems that arise due to network loss, service unavailability.
Such faults would normally resolve on their own after some time and won’t remain for a permanent period.
Recently, there have been 2 major releases of Polly: v7 and v8
Both are being supported in parallel
v8 is a major redesign of the Polly API with lots of enhancements and changes in the structure
API Design Changes from Polly v7 to v8
Here is a list that highlights the key changes that occurred to Polly going from v7 to v8:
- It is async-first by default
- Use of resilience strategy instead of resilience policy
- ResiliencePipelineBuilder was introduced to align with the resilience strategy
- Can easily combine multiple strategies using FluentBuilder
- Better customization and readability with PredicateBuilder to define when a strategy applies
- Easily configure the behavior with Strategy-specific options (like RetryStrategyOptions, TimeoutStrategyOptions)
- Seamless integration with HttpClient and HttpClientFactory
- Works seamlessly with the new resilience extensions within the Microsoft Library
Polly and the Resilience Extensions
Following the success of Polly, the recent version, v8, was re-architected to integrate with Microsoft.Extensions.Resilience and Microsoft.Extensions.Http.Resilience
These extensions provide an integrated set of revised and simplified APIs to help you leverage resilience strategies powered by Polly
Using Polly through Resilience Extensions
Add the resilience extensions NuGet Packages:

Then, inside the app builder services collection, define resilience strategy under a named HttpClient:
1 2 3 4 5 6 7 8 9 10 11 12 |
// Add a named HTTP client with Polly v8 resilience pipeline builder.Services.AddHttpClient("TestApi") .AddResilienceHandler("test-api-pipeline", builder => { builder.AddRetry(new() { MaxRetryAttempts = 3, Delay = TimeSpan.FromSeconds(2) }); builder.AddTimeout(TimeSpan.FromSeconds(5)); }); |
Now whenever you use the named HttpClient “TestApi” it will apply the resilience strategy that we defined earlier:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using System.Net.Http; using System.Threading.Tasks; public class TestApiService(IHttpClientFactory httpClientFactory) : ITestApiService { public async Task<string> GetDataAsync() { var client = httpClientFactory.CreateClient("TestApi"); //response will be returned after 2 seconds, this API can take delay up to 10s. var response = await client.GetAsync("https://httpbin.org/delay/2"); if (!response.IsSuccessStatusCode) { throw new HttpRequestException("Failed to get data from test API."); } return await response.Content.ReadAsStringAsync(); } } |
And of course when need to have the interface ready for the previous code to compile
1 2 3 4 |
public interface ITestApiService { Task<string> GetDataAsync(); } |
6 Strategies for Resilience in .NET
Polly offers 6 strategies for resilience in .NET, listed under 2 main categories: Reactive and Proactive
Reactive Strategies
1. Retry
Retries a failed operation multiple times before giving up.
Useful when errors are temporary and a quick retry could succeed.
In retry you can set the max number of attempts, the delay before the first attempt, and what’s the interval of delay between each retry: constant, exponential, or linear
Best used with:
- Transient network errors (e.g., HTTP 500, timeouts)
- Temporary unavailability of a downstream service
- Flaky third-party APIs
1 2 3 4 5 6 |
.AddRetry(new RetryStrategyptions { MaxRetryAttempts = 3, Delay = TimeSpan.FromMilliseconds(200), BackoffType = DelayBackoffType.Exponential }) |
2. Circuit Breaker
Monitors failures and stops sending requests once failure rate crosses a threshold.
Gives the system time to recover.
Best used to:
- Prevent hammering a failing system
- Avoid exhausting resources on repeated errors
- Enable graceful recovery and health checks
1 2 3 4 5 6 7 |
.AddCircuitBreaker(new CircuitBreakerStrategyOptions { FailureRatio = 0.5, MinimumThroughput = 5, SamplingDuration = TimeSpan.FromSeconds(30), BreakDuration = TimeSpan.FromSeconds(15) }) |
3. Fallback
When all else fails (retry, timeout, etc.), returns a safe default response instead of crashing.
Best when used to:
- Gracefully handle complete failure
- Return cached/stubbed data instead of throwing errors
- Maintain user experience even when services are down
1 2 3 4 5 6 7 8 |
.AddFallback(new FallbackStrategyOptions { ShouldHandle = new PredicateBuilder().Handle<TimeoutRejectedException>(), FallbackAction = static _ => ValueTask.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("This is a fallback response.") }) }) |
4. Timeout
Defines a maximum allowed time for an operation. If it takes too long, it’s forcefully canceled.
Best used to:
- Protect against long-hanging requests
- Prevent a system from freezing due to stuck dependencies
- Maintain snappy UX and system responsiveness
1 2 3 4 |
.AddTimeout(new TimeoutStrategyOptions { Timeout = TimeSpan.FromSeconds(3) }); |
5. Hedging
Sends backup/parallel requests after a short delay if the primary request is taking too long.
Improves response speed for unreliable services.
Best used with:
- High-latency or unreliable endpoints
- Mission-critical systems where waiting too long is costly
- Redundant endpoints or mirror services
1 2 3 4 5 6 7 8 9 10 11 12 |
.AddHedging(new HedgingStrategyOptions { MaxHedgedAttempts = 2, HedgingDelay = TimeSpan.FromMilliseconds(100), ShouldHandle = new PredicateBuilder().Handle<Exception>(), HedgingActionGenerator = args => ValueTask.FromResult<HedgingAction>( async token => { Console.WriteLine("Hedged action triggered"); await Task.Delay(200, token); }) }); |
6. Rate Limiter
Controls the number of allowed calls per time window.
Helps protect services from being overwhelmed by traffic.
Other modes include:
- Concurrency(n)
- TokenBucket(…)
- FixedWindow(…)
1 2 3 4 5 6 |
.AddRateLimiter(new RateLimiterStrategyOptions { PermitLimit = 5, Window = TimeSpan.FromSeconds(10), QueueLimit = 2 }); |
Strong Resilience Pipeline Setup
This is how you can change the strategies all together:
Here’s an example combining:
- Timeout – Fail fast if too slow.
- Retry – Retry on transient errors.
- Circuit Breaker – Stop calling a broken service.
- Fallback – Provide default or cached response if all else fails.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http.Resilience; using Microsoft.Extensions.Resilience; using Polly; using Polly.Retry; using Polly.CircuitBreaker; using Polly.Timeout; using Polly.Fallback; using System.Net; var builder = WebApplication.CreateBuilder(args); // Register HttpClient with a strong resilience pipeline builder.Services .AddHttpClient("TestApi", client => { client.BaseAddress = new Uri("https://httpbin.org/"); }) .AddResilienceHandler("strong-pipeline", static pipelineBuilder => { // Timeout strategy (first line of defense) pipelineBuilder.AddTimeout(TimeSpan.FromSeconds(3)); // Retry strategy pipelineBuilder.AddRetry(new RetryStrategyOptions<HttpResponseMessage> { MaxRetryAttempts = 3, Delay = TimeSpan.FromMilliseconds(500), BackoffType = DelayBackoffType.Exponential, ShouldHandle = args => { if (args.Outcome.Result is HttpResponseMessage response) { return ValueTask.FromResult( response.StatusCode == HttpStatusCode.InternalServerError || response.StatusCode == HttpStatusCode.RequestTimeout || response.StatusCode == HttpStatusCode.ServiceUnavailable || response.StatusCode == HttpStatusCode.BadGateway || response.StatusCode == HttpStatusCode.GatewayTimeout ); } return ValueTask.FromResult(false); } }); // Circuit Breaker strategy pipelineBuilder.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage> { FailureRatio = 0.5, MinimumThroughput = 4, SamplingDuration = TimeSpan.FromSeconds(10), BreakDuration = TimeSpan.FromSeconds(15), ShouldHandle = args => { if (args.Outcome.Result is HttpResponseMessage response) { return ValueTask.FromResult(!response.IsSuccessStatusCode); } return ValueTask.FromResult(false); } }); // Fallback strategy pipelineBuilder.AddFallback(new FallbackStrategyOptions<HttpResponseMessage> { ShouldHandle = args => { if (args.Outcome.Result is HttpResponseMessage response) { return ValueTask.FromResult(!response.IsSuccessStatusCode); } return ValueTask.FromResult(false); }, FallbackAction = _ => { var fallback = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("Fallback response from resilience pipeline.") }; return ValueTask.FromResult(Outcome.FromResult(fallback)); } }); }); var app = builder.Build(); // Sample endpoint to simulate transient failure (e.g., status/500) app.MapGet("/", async (IHttpClientFactory httpClientFactory) => { var client = httpClientFactory.CreateClient("MyClient"); var response = await client.GetAsync("status/500"); var content = await response.Content.ReadAsStringAsync(); return Results.Content(content); }); app.Run(); |
Summary
In this article, we explored how to build resilient .NET applications using Polly v8 through the Microsoft.Extensions.Resilience
and Microsoft.Extensions.Http.Resilience
packages. We discussed each of the core resilience strategies—Retry, Circuit Breaker, Fallback, Timeout, Hedging, and Rate Limiter, and demonstrated how to configure them using the new pipeline-based builder syntax of the resilience extensions.
Through practical examples, we showed how to:
- Automatically retry transient HTTP failures
- Protect systems under failure using circuit breakers
- Return controlled fallback responses
- Prevent long-hanging requests with timeouts
- Improve response times with hedged calls
- Throttle request flow via rate limiting
By integrating these strategies using the new built-in extensions (introduced in .NET 8), developers can create robust, fault-tolerant, and cloud-ready systems without manually managing Polly’s complexity.
The new approach encourages composability, observability, and strong alignment with modern .NET development practices.
References
You can always learn more by reading the official docs and checking other articles and tutorials:
Introduction to resilient app development
Build resilient HTTP apps: Key development patterns
Check my latest article: Your Quick Guide to JWT
Bonus
Here is a recommended listen for a beautiful rendition for Chopin’s 5 Most Popular Pieces