Hey friends! 👋
Ever had your app crash because a third-party API had a hiccup? Or watched users get errors because of a temporary network blip? Today we're going to make your HTTP calls bulletproof with resilience patterns!
The Problem: Networks Are Unreliable
The internet is held together by hope and duct tape. Services go down, networks get congested, and that API you depend on? It's going to fail eventually.
🎲 Fun Fact: Amazon found that even 100ms of latency costs them 1% in sales. Netflix reports that 0.1% of their requests fail due to transient errors. If the big players deal with this, so will you!
Enter Polly (and the New .NET 8+ Standard)
For years, Polly has been the go-to library for resilience in .NET. But with .NET 8, Microsoft introduced Microsoft.Extensions.Http.Resilience — a first-party solution built on Polly v8!
Option 1: Microsoft.Extensions.Http.Resilience (.NET 8+)
The new standard way — minimal config, sensible defaults:
// Install: dotnet add package Microsoft.Extensions.Http.Resilience
services.AddHttpClient<IMyApiClient, MyApiClient>()
.AddStandardResilienceHandler();
That's it! You get:
- ⏱️ Timeout: 30 seconds total
- 🔄 Retry: 3 attempts with exponential backoff
- ⚡ Circuit Breaker: Opens after 10% failure rate
- 🎚️ Rate Limiter: Prevents overwhelming the target
Customize It
services.AddHttpClient<IMyApiClient, MyApiClient>()
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 5;
options.Retry.Delay = TimeSpan.FromMilliseconds(500);
options.CircuitBreaker.FailureRatio = 0.1; // 10% failures trips it
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(10);
});
Option 2: Raw Polly (Full Control)
Need more control? Use Polly directly:
// Install: dotnet add package Microsoft.Extensions.Http.Polly
var retryPolicy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
onRetry: (outcome, delay, attempt, ctx) =>
{
Console.WriteLine($"Retry {attempt} after {delay.TotalSeconds}s");
});
services.AddHttpClient<IMyApiClient, MyApiClient>()
.AddPolicyHandler(retryPolicy);
The Circuit Breaker Pattern
This is my favorite pattern. Think of it like an electrical circuit breaker:
- Closed (normal): Requests flow through
- Open (tripped): Requests fail immediately — don't waste time on a dead service
- Half-Open (testing): Let one request through to check if service recovered
var circuitBreaker = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => r.StatusCode == HttpStatusCode.ServiceUnavailable)
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (result, duration) =>
{
Console.WriteLine($"Circuit OPEN for {duration.TotalSeconds}s!");
},
onReset: () => Console.WriteLine("Circuit CLOSED - back to normal"),
onHalfOpen: () => Console.WriteLine("Circuit HALF-OPEN - testing..."));
💡 Pro Tips
1. Add Jitter to Retries
Without jitter, all your retrying clients hit the recovering server at the same moment. Add randomness:
services.AddHttpClient<IMyApiClient, MyApiClient>()
.AddStandardResilienceHandler(options =>
{
options.Retry.UseJitter = true; // Adds randomness to retry delays
});
2. Be Idempotency-Aware
Only retry operations that are safe to repeat! A GET is fine, but retrying a POST that creates an order? You might create duplicates.
// Good: Retry GETs
var response = await _client.GetAsync("/api/orders/123");
// Careful: Don't blindly retry POSTs
// Use idempotency keys or check if operation already completed
3. Timeout Correctly
Set both per-attempt AND total timeouts:
services.AddHttpClient<IMyApiClient, MyApiClient>()
.AddStandardResilienceHandler(options =>
{
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5); // Per attempt
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30); // Overall
});
Real-World Example
services.AddHttpClient<IPaymentGateway, PaymentGateway>(client =>
{
client.BaseAddress = new Uri("https://api.stripe.com/");
})
.AddStandardResilienceHandler(options =>
{
// Payment APIs need quick failures
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(15);
// Retry on transient errors only
options.Retry.MaxRetryAttempts = 2;
options.Retry.ShouldHandle = args => ValueTask.FromResult(
args.Outcome.Exception is HttpRequestException ||
args.Outcome.Result?.StatusCode == HttpStatusCode.TooManyRequests);
});
Wrapping Up
Stop hoping your HTTP calls will work. Plan for failure, and your app will gracefully handle the chaos of distributed systems.
Start with AddStandardResilienceHandler() in .NET 8+ — it's an excellent default. Then customize as you learn your services' failure patterns.
Your users will never know how many retries and circuit breaker trips happen behind the scenes. They'll just see a reliable app! 🎉
Happy resilient coding! 🚀
Top comments (0)