Skip to main content
Resilience strategies are the building blocks of Polly. They execute user-defined callbacks while adding layers of fault tolerance. Strategies cannot run independently—they must be executed through a resilience pipeline.

Strategy Categories

Polly organizes resilience strategies into two main categories:

Reactive Strategies

Handle specific exceptions or results returned by callbacks. These strategies respond to failures after they occur.

Proactive Strategies

Make proactive decisions to cancel or reject callback execution before failures occur, like rate limiting or timeouts.

Built-in Reactive Strategies

Reactive strategies respond to failures by handling specific exceptions or results:
Premise: Many faults are transient and may self-correct after a short delay.How it helps: Automatically retries failed operations with configurable delay strategies (constant, linear, exponential with jitter).Use when: Network calls fail temporarily, database connections drop, or services experience brief outages.
var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromSeconds(2),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true
    })
    .Build();
Learn more about Retry →
Premise: When a system is seriously struggling, failing fast is better than making users wait. Protecting a faulting system from overload helps it recover.How it helps: Blocks executions when failures exceed a threshold, then periodically allows test requests to check if the system has recovered.Use when: You need to prevent cascading failures and give downstream services time to recover.
var pipeline = new ResiliencePipelineBuilder()
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions
    {
        FailureRatio = 0.5,
        SamplingDuration = TimeSpan.FromSeconds(10),
        MinimumThroughput = 8,
        BreakDuration = TimeSpan.FromSeconds(30)
    })
    .Build();
Learn more about Circuit Breaker →
Premise: Things will still fail—plan what you will do when that happens.How it helps: Defines an alternative value to return or action to execute when operations fail.Use when: You can provide default data, cached responses, or alternative implementations.
var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddFallback(new FallbackStrategyOptions<HttpResponseMessage>
    {
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .Handle<HttpRequestException>(),
        FallbackAction = args => 
            Outcome.FromResultAsValueTask(GetCachedResponse())
    })
    .Build();
Learn more about Fallback →
Premise: Things can be slow sometimes—plan what you will do when that happens.How it helps: Executes parallel actions when things are slow and returns the fastest successful result.Use when: You have multiple endpoints that can serve the same data, and latency is critical.
var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddHedging(new HedgingStrategyOptions<HttpResponseMessage>
    {
        MaxHedgedAttempts = 3,
        Delay = TimeSpan.FromSeconds(1)
    })
    .Build();
Learn more about Hedging →

Built-in Proactive Strategies

Proactive strategies prevent failures by controlling execution:
Premise: Beyond a certain wait time, a successful result is unlikely.How it helps: Guarantees the caller won’t wait beyond the timeout period.Use when: You need to enforce time limits on operations to maintain system responsiveness.
var pipeline = new ResiliencePipelineBuilder()
    .AddTimeout(new TimeoutStrategyOptions
    {
        Timeout = TimeSpan.FromSeconds(5)
    })
    .Build();
Learn more about Timeout →
Premise: Limiting the rate a system handles requests is another way to control load on your system or downstream services.How it helps: Constrains executions to not exceed a certain rate.Use when: You need to throttle incoming requests or limit the rate of calls to downstream APIs.
var pipeline = new ResiliencePipelineBuilder()
    .AddRateLimiter(new RateLimiterStrategyOptions
    {
        PermitLimit = 100,
        QueueLimit = 50
    })
    .Build();
Learn more about Rate Limiter →

Strategy Availability

Not all strategies are available for both generic and non-generic pipelines:
StrategyResiliencePipelineBuilderResiliencePipelineBuilder<T>
Circuit Breaker
Fallback
Hedging
Rate Limiter
Retry
Timeout
Fallback and Hedging strategies require a result type (T) because they need to return or handle specific result values.

Adding Strategies to Pipelines

Each resilience strategy provides extension methods for adding it to pipeline builders:
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
    .AddTimeout(new TimeoutStrategyOptions
    {
        Timeout = TimeSpan.FromSeconds(5)
    })
    .Build();
Configuration options are automatically validated and come with sensible defaults. You only need to specify properties that differ from the defaults.

Fault Handling with Predicates

Reactive strategies use the ShouldHandle predicate to determine which failures to handle. You can configure this in two ways: Switch expressions provide maximum flexibility:
var options = new RetryStrategyOptions<HttpResponseMessage>
{
    ShouldHandle = args => args.Outcome switch
    {
        { Exception: HttpRequestException } => PredicateResult.True(),
        { Exception: TimeoutRejectedException } => PredicateResult.True(),
        { Result: HttpResponseMessage response } when !response.IsSuccessStatusCode 
            => PredicateResult.True(),
        _ => PredicateResult.False()
    }
};

Using PredicateBuilder

The PredicateBuilder provides a fluent API for simpler scenarios:
var options = new RetryStrategyOptions<HttpResponseMessage>
{
    ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
        .HandleResult(response => !response.IsSuccessStatusCode)
        .Handle<HttpRequestException>()
        .Handle<TimeoutRejectedException>()
};
Using PredicateBuilder has a minor performance impact compared to manual predicates, as each method call registers a separate predicate that must be invoked.

Asynchronous Predicates

You can use async predicates for advanced scenarios like checking response bodies:
var options = new RetryStrategyOptions<HttpResponseMessage>
{
    ShouldHandle = async args =>
    {
        if (args.Outcome.Exception is not null)
        {
            return args.Outcome.Exception switch
            {
                HttpRequestException => true,
                TimeoutRejectedException => true,
                _ => false
            };
        }

        // Check response body asynchronously
        return await ShouldRetryAsync(
            args.Outcome.Result!, 
            args.Context.CancellationToken);
    }
};

Combining Strategies

You can combine multiple strategies in a single pipeline. The order matters:
// Circuit breaker on outside to fail fast
// Retry in middle to handle transient failures  
// Timeout on inside for per-attempt limits
var pipeline = new ResiliencePipelineBuilder()
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions
    {
        FailureRatio = 0.5,
        SamplingDuration = TimeSpan.FromSeconds(10),
        MinimumThroughput = 5,
        BreakDuration = TimeSpan.FromSeconds(30)
    })
    .AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromSeconds(1),
        BackoffType = DelayBackoffType.Exponential
    })
    .AddTimeout(TimeSpan.FromSeconds(5))
    .Build();

Strategy Configuration Best Practices

1

Use collections for exception types

Instead of chaining multiple .Handle<T>() calls, group exceptions:
ImmutableArray<Type> retryableExceptions = 
[
    typeof(SocketException),
    typeof(HttpRequestException),
    typeof(TimeoutRejectedException)
];

var options = new RetryStrategyOptions
{
    ShouldHandle = args =>
        ValueTask.FromResult(args.Outcome.Exception is not null &&
        retryableExceptions.Contains(args.Outcome.Exception.GetType()))
};
2

Define separate strategies for different failure domains

Don’t mix network failures with parsing failures in one strategy:
// Separate: Network calls
var networkPipeline = new ResiliencePipelineBuilder()
    .AddRetry(new() { 
        ShouldHandle = new PredicateBuilder()
            .Handle<HttpRequestException>() 
    })
    .Build();

// Separate: Processing
var processingPipeline = new ResiliencePipelineBuilder<Foo>()
    .AddTimeout(TimeSpan.FromMinutes(1))
    .Build();
3

Configure sensible defaults

All strategy options come with sensible defaults. Override only what you need:
// This works and uses sensible defaults
var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new())
    .Build();

// Customize only what you need
var pipeline2 = new ResiliencePipelineBuilder()
    .AddRetry(new() 
    { 
        MaxRetryAttempts = 5,  // Override default of 3
        UseJitter = true       // Override default of false
        // Other properties use defaults
    })
    .Build();

Quick Reference

Here’s a quick guide to choosing the right strategy:
ScenarioRecommended Strategy
Network call might fail temporarilyRetry
Operation might take too longTimeout
Service is completely downCircuit Breaker
Need to return default data on failureFallback
Want fastest response from multiple sourcesHedging
Need to control request rateRate Limiter
Multiple concurrent requestsConcurrency Limiter

Next Steps

Resilience Pipelines

Learn how to compose strategies into pipelines

Resilience Context

Understand how to share data across strategy execution

Retry Strategy

Deep dive into the retry strategy

Circuit Breaker Strategy

Explore circuit breaker patterns

Build docs developers (and LLMs) love