Overriding Default HTTP Resilience Handlers in .NET

Overriding Default HTTP Resilience Handlers in .NET

3 min read··

dotnethttpclientpollyresilience

ConfigureHttpClientDefaults with AddStandardResilienceHandler applies one Polly pipeline to every HttpClient, and it overrides any per-client pipeline you add afterwards. The workaround is an extension method that strips every ResilienceHandler from the client before you add your own. The .NET team merged a pull request adding proper support for a future release.

Have you ever had an outage caused by your Postgres DB? Did you max out resources? Neon is Postgres with autoscaling. Connections? Neon has pgBouncer built in for 10k connections. Failed migration? Neon has instant PITR and test environments. Get the Free Plan on Azure here.

Bufstream is the only cloud-native Kafka implementation independently validated by Jepsen, passing the gold standard for distributed systems testing. It's built for the modern enterprise—stateless, auto-scaling, schema-aware, and 10x cheaper than self-managed Kafka. Designed for high-throughput workloads, Bufstream is ready for organizations that struggle with Kafka scalability, cloud cost control, and data quality. Find out how Bufstream can improve your Kafka implementation in this article.

Introducing .NET 8 resilience packages built on top of Polly has made it much easier to build robust HTTP clients. These packages provide standard resilience handlers that you can easily attach to HttpClient instances. They implement common patterns like retry, circuit breaker, and timeout policies.

However, there is a significant limitation: once you configure the standard resilience handlers globally for all clients, there is no built-in way to override them for specific cases. This can be problematic when different endpoints require different resilience strategies.

In today's issue, I'll show you how to fix this and what the .NET team is doing about it.

Standard Resilience Configuration

Let's say you've configured default resilience handlers in your application startup. ConfigureHttpClientDefaults is a convenient way to add standard resilience handlers to all HttpClient instances:

builder.Services
    .AddHttpClient()
    .ConfigureHttpClientDefaults(http => http.AddStandardResilienceHandler());

The .NET team runs many large-scale services in production, and they've found a standard set of resilience strategies that work well for most scenarios.

The standard resilience handler combines five strategies to create a resilience pipeline:

  • Rate limiter
  • Total request timeout
  • Retry
  • Circuit breaker
  • Attempt timeout

You can customize the standard resilience pipeline by configuring the HttpStandardResilienceOptions.

Here's an example of how to configure it:

builder.Services.ConfigureHttpClientDefaults(http => http.AddStandardResilienceHandler(options =>
{
    // Default is 2 seconds.
    options.Retry.Delay = TimeSpan.FromSeconds(1);

    // Default is 30 seconds.
    options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(20);

    // Default is 0.1.
    options.CircuitBreaker.FailureRatio = 0.2;
}));

Okay, so we have our standard resilience pipeline set up. Now all your HttpClient instances will use these resilience policies.

But what if you need different retry logic for a specific API endpoint or need to turn off circuit breaking for specific calls?

The Problem

Let's say you have a named HttpClient for calling the GitHub API, and you want to configure specific resilience strategies for it:

builder.Services
    .AddHttpClient("github")
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri("https://api.github.com");
    })
    .AddResilienceHandler("custom", pipeline =>
    {
        pipeline.AddTimeout(TimeSpan.FromSeconds(10));

        pipeline.AddRetry(new HttpRetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            BackoffType = DelayBackoffType.Exponential,
            UseJitter = true,
            Delay = TimeSpan.FromMilliseconds(500)
        });

        pipeline.AddTimeout(TimeSpan.FromSeconds(1));
    });

The custom policy won't be applied because we have a global resilience pipeline that overrides it.

This is a big oversight in the current implementation of the .NET resilience packages.

The Solution

The solution is to create an extension method that clears all handlers from the resilience pipeline. This allows you to remove the default handlers and add your custom ones.

Here's how to implement it:

public static class ResilienceHttpClientBuilderExtensions
{
    public static IHttpClientBuilder RemoveAllResilienceHandlers(this IHttpClientBuilder builder)
    {
        builder.ConfigureAdditionalHttpMessageHandlers(static (handlers, _) =>
        {
            for (int i = handlers.Count - 1; i >= 0; i--)
            {
                if (handlers[i] is ResilienceHandler)
                {
                    handlers.RemoveAt(i);
                }
            }
        });
        return builder;
    }
}

Now you can use this extension method to implement custom resilience strategies:

builder.Services
    .AddHttpClient("github")
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri("https://api.github.com");
    })
    .RemoveAllResilienceHandlers()
    .AddResilienceHandler("custom", pipeline =>
    {
        // Configure the custom resilience pipeline...
    });

// Or use another standard resilience pipeline...
builder.Services
    .AddHttpClient("github-hedged")
    .RemoveAllResilienceHandlers()
    .AddStandardHedgingHandler();

Future Improvements

The .NET team is aware of this limitation, and better support for overriding default resilience handlers is planned for an upcoming release. The pull request for this API is merged and should be available in a future release.

Until then, this workaround using RemoveAllResilienceHandlers is a drop-in replacement for the missing feature.

Conclusion

The ability to override default resilience handlers is much needed when building robust distributed systems. While .NET's standard resilience handlers provide excellent defaults, real-world applications often require fine-tuned resilience strategies for different services. The extension method presented here bridges this gap, allowing you to maintain both global defaults and specialized configurations where needed.

Want to dive deeper into building resilient cloud applications? Check out my article about building resilient cloud applications with .NET.

Good luck out there, and see you next week.


Frequently Asked Questions

What does the standard resilience handler in .NET include?

The .NET 8 standard resilience handler, built on Polly, combines five strategies into one pipeline: rate limiter, total request timeout, retry, circuit breaker, and attempt timeout. You attach it to every HttpClient with ConfigureHttpClientDefaults and AddStandardResilienceHandler.

Why is a custom AddResilienceHandler pipeline not applied in .NET?

When a standard resilience handler was configured globally through ConfigureHttpClientDefaults, it overrode any client-specific pipeline added with AddResilienceHandler. As of early 2025 the .NET resilience packages had no built-in way to override the global defaults for a specific client.

How do you override the default HTTP resilience handlers for one HttpClient?

Add an extension method that iterates the client's additional message handlers and removes every ResilienceHandler, then call it before AddResilienceHandler. With the inherited defaults cleared, the client-specific pipeline (or a standard hedging handler) takes effect.

How do you customize the standard resilience handler in .NET?

Pass a configuration delegate to AddStandardResilienceHandler and set properties on HttpStandardResilienceOptions. The defaults were a 2-second retry delay, a 30-second total request timeout, and a 0.1 circuit breaker failure ratio, and each one can be changed.

Did the .NET team fix the resilience handler override limitation?

As of early 2025, the .NET team had merged a pull request in dotnet/extensions adding proper support for overriding default resilience handlers, planned for a future release. Until it shipped, a RemoveAllResilienceHandlers extension method served as a drop-in workaround.

Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

Become a Better .NET Software Engineer

Join 66,000+ engineers who are improving their skills every Saturday morning.