Skip to main content
The ResiliencePipeline is the foundation of Polly’s resilience framework. It allows you to execute arbitrary user-provided callbacks while applying one or more resilience strategies to handle transient faults and failures.

What is a Resilience Pipeline?

A resilience pipeline is a combination of one or more resilience strategies that work together to make your application more fault-tolerant. Think of it as a wrapper around your code that adds layers of protection against failures.
A resilience pipeline can contain multiple strategies like retry, circuit breaker, timeout, and rate limiter, all working in harmony to protect your application.

Creating a Resilience Pipeline

You create resilience pipelines using the ResiliencePipelineBuilder. Here’s a simple example:
using Polly;

// Create a pipeline with a concurrency limiter
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
    .AddConcurrencyLimiter(100)
    .Build();

Executing Code with Pipelines

Once you’ve built a pipeline, you can execute various types of callbacks:
1

Execute asynchronous void callbacks

await pipeline.ExecuteAsync(
    async token => await MyMethodAsync(token),
    cancellationToken);
2

Execute synchronous void callbacks

pipeline.Execute(() => MyMethod());
3

Execute callbacks that return values

var response = await pipeline.ExecuteAsync(
    async token => await httpClient.GetAsync(endpoint, token),
    cancellationToken);
4

Execute without lambda allocation (performance optimization)

await pipeline.ExecuteAsync(
    static async (state, token) => await state.httpClient.GetAsync(state.endpoint, token),
    (httpClient, endpoint),  // State provided here
    cancellationToken);

Dependency Injection Integration

In production applications, you should separate pipeline definition from usage. Polly integrates seamlessly with .NET dependency injection:
public static void ConfigureMyPipelines(IServiceCollection services)
{
    services.AddResiliencePipeline("pipeline-A", builder => 
        builder.AddConcurrencyLimiter(100));
    
    services.AddResiliencePipeline("pipeline-B", builder => 
        builder.AddRetry(new()));

    // Later, resolve the pipeline by name
    var pipelineProvider = services.BuildServiceProvider()
        .GetRequiredService<ResiliencePipelineProvider<string>>();
    
    pipelineProvider.GetPipeline("pipeline-A").Execute(() => { });
}
Defining pipelines at startup using AddResiliencePipeline is the recommended approach for .NET applications. This makes them easily testable and injectable.

Empty Resilience Pipeline

Polly provides a special empty pipeline that contains no resilience strategies:
ResiliencePipeline.Empty
ResiliencePipeline<T>.Empty
The empty pipeline is particularly useful in test scenarios where implementing resilience strategies could slow down test execution or complicate test setup.

Advanced: Retrieving Results with Outcome

For high-performance scenarios, use ExecuteOutcomeAsync to avoid throwing exceptions:
// Acquire a ResilienceContext from the pool
ResilienceContext context = ResilienceContextPool.Shared.Get();

// Execute the pipeline and store the result in an Outcome<bool>
Outcome<bool> outcome = await pipeline.ExecuteOutcomeAsync(
    static async (context, state) =>
    {
        Console.WriteLine("State: {0}", state);

        try
        {
            await MyMethodAsync(context.CancellationToken);
            return Outcome.FromResult(true);
        }
        catch (Exception e)
        {
            return Outcome.FromException<bool>(e);
        }
    },
    context,
    "my-state");

// Return the acquired ResilienceContext to the pool
ResilienceContextPool.Shared.Return(context);

// Evaluate the outcome
if (outcome.Exception is not null)
{
    Console.WriteLine("Execution Failed: {0}", outcome.Exception.Message);
}
else
{
    Console.WriteLine("Execution Result: {0}", outcome.Result);
}
Use ExecuteOutcomeAsync in high-performance scenarios where you need to avoid the overhead of throwing and catching exceptions.

Context vs State

You might wonder about the difference between context and state parameters:

State Object

The state parameter is a performance optimization that allows you to pass data to your callback without using closures. It’s only accessible inside your callback and enables the use of static anonymous methods.

Context Object

The context parameter is accessible throughout the entire pipeline execution, including in strategy delegates like ShouldHandle, OnRetry, and DelayGenerator. Use it to exchange information between different parts of the pipeline.

Rule of Thumb

  • Use state to pass parameters to your decorated method
  • Use context to exchange information between delegates or across retry/hedging attempts

How Strategies Work Together

When you add multiple strategies to a pipeline, they execute in the order you add them, creating layers of protection:
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new() { 
        ShouldHandle = new PredicateBuilder().Handle<TimeoutRejectedException>() 
    }) // outer
    .AddTimeout(TimeSpan.FromSeconds(1)) // inner
    .Build();

Execution Flow

1

Request enters the pipeline

The caller invokes ExecuteAsync on the pipeline.
2

Outer strategies execute first

The retry strategy (added first) wraps the timeout strategy.
3

Inner strategies execute next

The timeout strategy (added second) wraps your actual code.
4

Your code executes

Your callback runs with all the protection layers active.
5

Strategies handle failures

If a timeout occurs, the timeout strategy throws TimeoutRejectedException, which the retry strategy can catch and retry.

Visualizing Pipeline Execution

Here’s how a retry strategy wrapping a timeout strategy behaves when the first attempt times out but the second succeeds:

Best Practices

Define your pipelines at application startup and inject them where needed. This approach facilitates unit testing and makes your code more maintainable.
// At startup
services.AddResiliencePipeline("my-pipeline", builder => 
    builder.AddRetry(new()));

// In your service
public class MyService
{
    private readonly ResiliencePipeline _pipeline;
    
    public MyService(ResiliencePipelineProvider<string> provider)
    {
        _pipeline = provider.GetPipeline("my-pipeline");
    }
}
The order in which you add strategies affects behavior significantly. Generally:
  • Add timeout around retry to limit total execution time
  • Add retry around timeout to retry individual attempts that time out
  • Add circuit breaker on the outside to fail fast when a service is down
// Total timeout of 10 seconds, with per-attempt timeout of 1 second
var pipeline = new ResiliencePipelineBuilder()
    .AddTimeout(TimeSpan.FromSeconds(10))  // Total time budget
    .AddRetry(new())                       // Retry logic
    .AddTimeout(TimeSpan.FromSeconds(1))   // Per-attempt timeout
    .Build();
Reuse ResilienceContext instances from the pool to reduce allocations:
ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken);
try
{
    await pipeline.ExecuteAsync(async ctx => { /* your code */ }, context);
}
finally
{
    ResilienceContextPool.Shared.Return(context);
}

Common Pipeline Patterns

// Retries up to 3 times, each attempt times out after 1 second
var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new() { 
        ShouldHandle = new PredicateBuilder()
            .Handle<TimeoutRejectedException>() 
    })
    .AddTimeout(TimeSpan.FromSeconds(1))
    .Build();

Next Steps

Resilience Strategies

Learn about the different types of strategies you can add to pipelines

Resilience Context

Understand how to pass data through pipeline execution

Build docs developers (and LLMs) love