Skip to content
ExponaBLOG
.NET & ASP.NET CORE · DAY 043 / 100

Resilience with Microsoft.Extensions.Http.Resilience

Retries, timeouts and circuit breakers in one line.

Expona Admin5 min read

Resilience with Microsoft.Extensions.Http.Resilience

When your ASP.NET Core application calls an external API, it's not a question of if something will fail—it's when. Network timeouts, transient service outages, and rate-limit responses are inevitable. I've watched payment integrations crumble during a gateway's scheduled maintenance window, and e-commerce orders get lost because we weren't handling HTTP failures properly. That's where Resilience with Microsoft.Extensions.Http.Resilience becomes invaluable: it wraps your HttpClient calls with battle-tested resilience patterns—retries, timeouts, circuit breakers, and rate limiting—all configured with a single line of code.

Built on Polly v8, Maintained by the .NET Team

Microsoft.Extensions.Http.Resilience isn't a new framework invented from scratch. It's built on top of Polly v8, the de facto standard for resilience in .NET for over a decade. What changed is the packaging: the .NET team now maintains an official, opinionated, streamlined version specifically for HTTP scenarios, baked directly into the Microsoft.Extensions ecosystem.

This matters because you're not guessing at configuration. The team has already tuned sensible defaults based on real-world production deployments. When you call AddStandardResilienceHandler(), you're getting a pipeline that Microsoft uses internally. I've run this in production across trading platforms, ride-hailing backends, and travel booking systems—it's solid.

The beauty is consistency. If you're already using dependency injection via IServiceCollection, resilience handlers slot in naturally. No new abstractions to learn, no custom middleware layering. It's idiomatic ASP.NET Core.

Standard Pipeline: Rate Limit, Timeout, Retry, Circuit Breaker

The standard resilience handler applies four policies in order:

  1. Rate Limiter: Prevents your client from overwhelming the downstream service.
  2. Timeout: Fails fast if the service doesn't respond within a threshold.
  3. Retry: Automatically retries transient failures with exponential backoff.
  4. Circuit Breaker: Stops sending requests if the service is consistently down.

Each policy guards against a different failure mode. Here's how they flow:

csharp

builder.Services.AddHttpClient<PaymentClient>()
  .AddStandardResilienceHandler();

That's it. Now every request through PaymentClient is wrapped with all four policies. Let me break down what happens under the hood.

Rate Limiter

If you're calling a third-party API with a quota, the rate limiter ensures you don't exceed it. By default, it's lenient—allowing most requests through—but it queues excess requests rather than failing them immediately. This prevents your client from becoming a bad citizen on the network.

Timeout

The timeout is set to 10 seconds by default. If your HTTP request doesn't complete in that window, it's cancelled and treated as a transient failure. This is critical: without a timeout, a slow or hung endpoint can exhaust your connection pool and cascade failures across your entire system. I've seen a single slow payment gateway call lock up an entire e-commerce checkout flow because we didn't have timeouts configured properly.

Retry

The retry policy automatically repeats failed requests up to 3 times (by default) with exponential backoff. Crucially, it only retries transient failures: HTTP 5xx responses, timeouts, and network errors. It won't retry 4xx responses like 400 Bad Request or 403 Forbidden, because retrying those is pointless.

Circuit Breaker

After a threshold of consecutive failures (default: 5), the circuit breaker trips and rejects new requests immediately for 30 seconds. This prevents cascading failures: if the payment gateway is down, you don't keep hammering it and exhausting resources. Instead, you fail fast and give the gateway time to recover.

Retry Only Idempotent Requests

Here's a critical gotcha: not all requests are safe to retry. If you retry a POST that creates a charge, you might create two charges. If you retry a DELETE, you might delete twice.

The standard handler is smart about this. It only retries GET and HEAD requests automatically. For other methods, it's your responsibility to ensure idempotency or disable retries.

In practice, I've had to customize resilience policies for payment integrations. Many payment gateways require an idempotency key—a unique identifier that ensures the same request can be safely replayed:

csharp

builder.Services.AddHttpClient<PaymentClient>()
  .AddStandardResilienceHandler()
  .ConfigureHttpClient(client =>
  {
    client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
  });

With an idempotency key, the gateway deduplicates requests on its side. If you retry, the gateway recognizes it's the same logical request and returns the same result. Safe to retry.

But what if the downstream service doesn't support idempotency? Then you need a custom policy. The Polly v8 foundation makes this straightforward:

csharp

builder.Services.AddHttpClient<OrderClient>()
  .AddStandardResilienceHandler()
  .AddPolicyHandler(GetCustomPolicy());

static IAsyncPolicy<HttpResponseMessage> GetCustomPolicy()
{
  return HttpPolicyExtensions
    .HandleTransientHttpError()
    .OrResult(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
    .WaitAndRetryAsync(
      retryCount: 3,
      sleepDurationProvider: attempt =>
        TimeSpan.FromSeconds(Math.Pow(2, attempt))
    );
}

Now you're only retrying 5xx errors and 429 (Too Many Requests). Everything else fails immediately, so you won't accidentally double-charge or double-create orders.

Saved Us During Payment Gateway Outages

I remember a Tuesday morning in 2023. Our primary payment gateway had a network issue. Requests were timing out every few seconds. Without resilience, customers would see checkout errors immediately, and support would flood with complaints.

With Resilience with Microsoft.Extensions.Http.Resilience, here's what happened:

  1. First request times out after 10 seconds. Retried.
  2. Second attempt succeeds within 2 seconds. Order processed.
  3. After 5 consecutive failures, the circuit breaker trips.
  4. We fail fast instead of cascading timeouts.
  5. Support team is alerted, not overwhelmed.
  6. 30 seconds later, the circuit breaker resets and probes the gateway. It's recovered. Traffic flows again.

We didn't lose a single order. Without proper resilience handling, we'd have seen checkout failures, connection pool exhaustion, and a complete service outage.

Another scenario: a ride-hailing backend calling a geolocation service that was under heavy load. The standard resilience handler rate-limited our requests to respect their quota, then retried with backoff. Rides were delayed, not dropped.

Resilience isn't just about surviving failures. It's about graceful degradation. When things go wrong—and they will—your application keeps limping forward instead of crashing.

Common Mistakes

  • Retrying non-idempotent operations without thought: Always ask: is this request safe to replay? If not, either make it idempotent (idempotency keys) or disable retries for that specific operation.
  • Setting timeouts too high or too low: A 60-second timeout defeats the purpose (you're waiting too long anyway). A 100ms timeout might be unrealistic for heavy computation. Test with your actual dependencies.
  • Forgetting to handle circuit breaker exceptions: When the circuit is open, you still get an exception. Catch HttpRequestException or BrokenCircuitException and return a fallback response or user-friendly error.
  • Not customizing for your use case: The standard pipeline is good for APIs. But if you're calling a WebSocket service or long-polling endpoint, you need different settings. Use ConfigureHttpClientDefaults to tune globally or AddPolicyHandler for per-client customization.

Key Takeaways

  • Resilience with Microsoft.Extensions.Http.Resilience brings Polly v8 resilience patterns into the official .NET ecosystem with sensible defaults.
  • The standard pipeline applies rate limiting, timeouts, retries, and circuit breaking in a logical order, protecting your application against transient failures and cascading outages.
  • Only retry idempotent requests (GET, HEAD) by default. For POST/PUT/DELETE, either implement idempotency keys or customize your retry policy.
  • Real-world wins: payment gateway outages, overloaded services, and network hiccups are handled gracefully. Your application degrades instead of crashes.
  • Start with AddStandardResilienceHandler(), then customize with ConfigureHttpClientDefaults or AddPolicyHandler as your service matures.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles