Microsoft Resiliency Extensions and Polly Part 1 - Building Your First Resilience Pipeline

July 10, 2025#Software Development
Article
Author image.

Jeff Zuerlein, Senior Consultant

Let’s suppose you’re working on an application that experiences transient faults—for example, a REST service you depend on occasionally fails to respond. The root cause might be infrastructure instability, network flakiness, or a downstream service that’s simply outside your control. You can’t fix the dependency, but you can change how your application responds to these failures. That’s where Microsoft’s resiliency extensions—and Polly—come to the rescue.

Polly is a popular open-source library that’s helped .NET developers implement robust fault-handling strategies like retries, timeouts, and circuit breakers for years. With the release of version 8, Polly was significantly redesigned for performance and composability, and now forms the foundation of the Microsoft.Extensions.Resilience and Microsoft.Extensions.Http.Resilience packages. In fact, if you dig into the dependencies of these libraries, you’ll find Polly right there powering the entire resilience pipeline.

Microsoft.Extensions.Http.Resilience

Microsoft has introduced extension methods that make it easy to apply standardized resilience strategies to HttpClient instances. For many scenarios, this can be a great shortcut—a simple, one-size-fits-most approach to building resilience into your HTTP calls.

services
    .AddHttpClient("my-client")
    .AddStandardResilienceHandler(options =>
    {
        // Customize retry logic
        options.Retry.ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .Handle<TimeoutRejectedException>()
            .Handle<HttpRequestException>()
            .HandleResult(response => response.StatusCode == HttpStatusCode.InternalServerError);
        
        options.Retry.MaxRetryAttempts = 5;

        // Customize attempt timeout
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(2);
    });

The AddStandardResilienceHandler method wires up a comprehensive resilience pipeline with built-in strategies for:

  • Rate limiting
  • Total request timeout
  • Retry logic
  • Circuit breaking
  • Per-attempt timeouts

That’s a lot of functionality implemented with just a few lines of code—and the options object gives you the flexibility to fine-tune each strategy as needed. Microsoft also provides several other standardized handler sets, each designed for common usage patterns. These can save time and ensure consistent resilience defaults for teams that want consistancy. But if your scenario calls for more control, or you want to implement resilience for non-HTTP code, you’re not limited to the built-in handlers. You can build custom pipelines using the same powerful engine under the hood: Polly’s ResiliencePipeline.

What Are Resilience Pipelines?

Implementing a resiliency strategy in .NET is accomplished using a ResiliencePipeline. This pipeline acts as a container for one or more resilience strategies—like retries, timeouts, rate limiting, or circuit breakers—and executes your code (such as HTTP requests or database queries) through them.

Let’s walk through an example that shows how to define a pipeline to make an HTTP request with a timeout and a retry policy in case the first attempt fails.

Defining a Custom Resilience Pipeline with DI

In this example, we’ll define a pipeline using an extension method on IServiceCollection. This is a convenient way to register a reusable pipeline in your application’s dependency injection container.

public static class ResiliencePipelineExtensions
{
    public static IServiceCollection AddCustomResiliencePipeline(this IServiceCollection services)
    {
        services.AddResiliencePipeline("http-pipeline", builder =>
        {
            // Retry strategy
            builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
            {
                MaxRetryAttempts = 3,
                ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                    .Handle<TimeoutRejectedException>()
                    .HandleResult(response => response.StatusCode == HttpStatusCode.InternalServerError)
            });

            // Timeout per attempt
            builder.AddTimeout(TimeSpan.FromSeconds(2));
        });

        return services;
    }
}

In this code:

  • ResiliencePipelineBuilder<T> is used to compose strategies like .AddRetry() and .AddTimeout().
  • RetryStrategyOptions<T> lets you configure how and when retries should happen:
  • MaxRetryAttempts controls how many times a failed attempt will be retried.
  • ShouldHandle defines the conditions that should trigger a retry.
  • AddTimeout accepts a TimeSpan that specifies how long the pipeline should allow the executing code to run before throwing a timeout exception.

PredicateBuilder<T> helps you compose readable conditions, such as retrying on a TimeoutRejectedException or a specific HttpStatusCode. This makes the retry logic expressive and easy to extend. You could just as easily handle other exceptions or HTTP response codes by chaining more conditions.

Understanding Strategy Order in a Resilience Pipeline

The order in which you add strategies to the pipeline matters. The first strategy added to the pipeline wraps the next one that is added. That means that the behaviour of the pipeline depends on the order. Looking at an example is the best way to understand why.

Original Strategy Order: Retry Outside, Timeout Inside

builder
    .AddRetry(...)        // outer
    .AddTimeout(...)      // inner

In this setup, each attempt is wrapped in a timeout. If the code takes longer than the timeout (e.g., 2 seconds), it throws a TimeoutRejectedException. The retry strategy catches that timeout exception and triggers another attempt.

Key behavior : Every retry gets its own full 2-second timeout. So if MaxRetryAttempts is 3, the operation could take up to 6 seconds total (3 × 2 seconds).

Reversed Order: Timeout Outside, Retry Inside

builder
    .AddTimeout(...)      // outer
    .AddRetry(...)        // inner

This changes everything. Now, the entire retry sequence must complete within the 2-second timeout. If the first attempt takes too long (say, 1.5 seconds), the retry may or may not happen before the 2-second timeout is hit. If a retry is attempted, it has to finish quickly, or the entire pipeline will timeout.

Key behavior : The timeout limits the total duration of all retries combined.

Choosing the Right Order

  • Retry outside, timeout inside (the default in most docs): Best for retrying individual transient failures that should be bounded in time.
  • Timeout outside, retry inside: Useful when you need to cap the total execution time strictly (e.g., SLA-driven systems or APIs with aggressive response-time budgets).

This subtle ordering can make a big difference in how your resilience strategy behaves. Fortunately, Polly v8’s ResiliencePipelineBuilder gives you full control.

Using a Resilience Pipeline with an HTTP Client

Now that we’ve set up our ResiliencePipeline, it’s time to use it in actual application code. In this example, I’m calling the National Weather Service’s REST API using a custom client class that leverages the Polly pipeline to handle transient failures cleanly. Since we registered the ResiliencePipeline in dependency injection, we can inject the ResiliencePipelineProvider into our class constructor. This provider gives us access to the named pipeline we registered earlier:

public class WeatherApiClient
{
    private readonly ResiliencePipeline _pipeline;

    public WeatherApiClient(ResiliencePipelineProvider<string> pipelineProvider)
    {
        _pipeline = pipelineProvider.GetPipeline("name-of-pipeline");
    }
}

Once you have an instance of the pipeline, you can use its ExecuteAsync method to wrap any operation you want to protect with your defined strategies. Here’s how that might look:

public async Task<HttpResponseMessage> GetAsync(string url, CancellationToken cancellationToken)
{
    using var httpClient = new HttpClient();

    return await _pipeline.ExecuteAsync<HttpResponseMessage>(
        async token => await httpClient.GetAsync(url, token));
}

Let’s break it down. We’re passing a callback function to ExecuteAsync that performs the actual HTTP GET request. The ResiliencePipeline will:

  • Timeout the request if it takes longer than 2 seconds (as configured earlier).
  • Retry the request (up to 2 times) if it fails with a timeout or a qualifying error like an HTTP 500.

All of that retry and timeout logic is defined once, when the pipeline is registered. That means your business logic stays clean, and resilience becomes a cross-cutting concern handled by configuration—not boilerplate code.

Why This Matters

By using ResiliencePipeline:

  • You gain consistency across all service calls.
  • Your retry logic is centralized and easy to test or modify.
  • You reduce the risk of copy-paste bugs from rolling your own retry/timeout logic repeatedly.

Conclusion

Building applications that can withstand transient failures—whether due to infrastructure hiccups or unreliable external services—is far easier when you leverage Microsoft’s Resilience APIs and Polly v8. These tools offer everything from ready-to-use, standardized HTTP strategies to fully customizable resilience pipelines.

By understanding how these pipelines work and how to configure them, you can design resilience strategies that are tailored to your application’s specific needs, rather than relying on hardcoded retry loops or ad-hoc error handling. In the next article, we’ll dive into telemetry and observability—exploring how you can monitor what’s happening inside your resilience pipelines, measure failures, and gain deeper insight into how your application behaves under stress.

Resources


Copyright © 2025 NimblePros - All Rights Reserved