DEV Community

Cover image for Building Bulletproof HTTP Calls with Polly in C#
Nick
Nick

Posted on AI-assisted

Building Bulletproof HTTP Calls with Polly in C#

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();
Enter fullscreen mode Exit fullscreen mode

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);
    });
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

The Circuit Breaker Pattern

This is my favorite pattern. Think of it like an electrical circuit breaker:

  1. Closed (normal): Requests flow through
  2. Open (tripped): Requests fail immediately — don't waste time on a dead service
  3. 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..."));
Enter fullscreen mode Exit fullscreen mode

💡 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
    });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    });
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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)