Skip to main content

Retry Strategy

The retry reactive resilience strategy re-executes the same callback method if its execution fails. Failure can be either an Exception or a result object indicating unsuccessful processing.
Between retry attempts, the retry strategy waits a specified amount of time. You have fine-grained control over how to calculate the next delay.

When to Use Retry

Use the retry strategy when:
  • Dealing with transient failures that are likely to self-correct
  • Calling remote services that may experience temporary network issues
  • Accessing resources that may be temporarily unavailable
  • Working with rate-limited APIs that return 429 (Too Many Requests) responses
Don’t use retry for operations that are not idempotent (operations that can be safely repeated without side effects).

Installation

dotnet add package Polly.Core

Usage

Basic Retry

// Retry using default options (3 retries with 2 second delay)
var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions())
    .Build();

await pipeline.ExecuteAsync(async token => 
{
    // Your operation that may fail
    await CallExternalServiceAsync(token);
}, cancellationToken);

Instant Retries

var options = new RetryStrategyOptions
{
    Delay = TimeSpan.Zero
};

Advanced Configuration

var options = new RetryStrategyOptions
{
    ShouldHandle = new PredicateBuilder()
        .Handle<HttpRequestException>(),
    BackoffType = DelayBackoffType.Exponential,
    UseJitter = true,  // Adds randomness to avoid thundering herd
    MaxRetryAttempts = 4,
    Delay = TimeSpan.FromSeconds(3),
};

Custom Delay Generator

var options = new RetryStrategyOptions
{
    MaxRetryAttempts = 2,
    DelayGenerator = static args =>
    {
        var delay = args.AttemptNumber switch
        {
            0 => TimeSpan.Zero,
            1 => TimeSpan.FromSeconds(1),
            _ => TimeSpan.FromSeconds(5)
        };
        
        return new ValueTask<TimeSpan?>(delay);
    }
};

Handling HTTP Responses

var options = new RetryStrategyOptions<HttpResponseMessage>
{
    ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
        .Handle<HttpRequestException>()
        .HandleResult(r => r.StatusCode == HttpStatusCode.TooManyRequests),
    DelayGenerator = static args =>
    {
        // Extract retry-after header from response
        if (args.Outcome.Result is HttpResponseMessage response &&
            response.Headers.RetryAfter?.Delta is TimeSpan delay)
        {
            return new ValueTask<TimeSpan?>(delay);
        }
        
        return new ValueTask<TimeSpan?>((TimeSpan?)null);
    }
};

Retry Events

var options = new RetryStrategyOptions
{
    MaxRetryAttempts = 2,
    OnRetry = static args =>
    {
        Console.WriteLine($"Retry attempt {args.AttemptNumber}");
        // Log the error, send metrics, etc.
        return default;
    }
};

Configuration Options

ShouldHandle
Predicate
Defines which results and/or exceptions should trigger a retry.
MaxRetryAttempts
int
default:"3"
The maximum number of retry attempts to use, in addition to the original call.
BackoffType
DelayBackoffType
default:"Constant"
The back-off algorithm type:
  • Constant: Same delay between retries
  • Linear: Delay increases linearly
  • Exponential: Delay increases exponentially
Delay
TimeSpan
default:"2 seconds"
The base delay between retry attempts. The actual delay depends on the BackoffType.
MaxDelay
TimeSpan?
default:"null"
If provided, caps the calculated retry delay to this value.
UseJitter
bool
default:"false"
If true, adds randomness to retry delays:
  • For Constant and Linear: adds ±25% random variation
  • For Exponential: uses Decorrelated Jitter Backoff V2 algorithm
DelayGenerator
Func<DelayGeneratorArguments, ValueTask<TimeSpan?>>
default:"null"
Dynamically calculates the retry delay. If this returns null, the strategy uses the calculated delay from other properties.
OnRetry
Func<OnRetryArguments, ValueTask>
default:"null"
Invoked before the strategy delays the next attempt. Useful for logging and metrics.

Backoff Types Explained

Constant Backoff

Same delay between all retry attempts.
// Settings: Delay = 1 second
// Result: [1000ms, 1000ms, 1000ms, 1000ms, 1000ms]

Linear Backoff

Delay increases by the base delay amount with each attempt.
// Settings: Delay = 1 second
// Result: [1000ms, 2000ms, 3000ms, 4000ms, 5000ms]

Exponential Backoff

Delay doubles with each attempt.
// Settings: Delay = 1 second
// Result: [1000ms, 2000ms, 4000ms, 8000ms, 16000ms]
Use exponential backoff with jitter for best results when dealing with external services. This prevents the “thundering herd” problem where many clients retry simultaneously.

Best Practices

Don’t retry too many times. 3-5 retries is usually sufficient for most scenarios. More retries increase latency and may overwhelm failing systems.
Always use UseJitter = true when calling external services to prevent synchronized retry storms from multiple clients.
Use MaxDelay to prevent exponential backoff from creating excessive wait times on later retry attempts.
Configure ShouldHandle to only retry temporary failures (network issues, timeouts) and not permanent failures (400 Bad Request, 401 Unauthorized).
Combine retry with circuit breaker to prevent retrying when a system is known to be down. Add timeout to prevent individual retry attempts from taking too long.

Common Patterns

Quick Retries Then Slow Retries

var quickRetries = new RetryStrategyOptions
{
    MaxRetryAttempts = 3,
    Delay = TimeSpan.FromMilliseconds(100),
    BackoffType = DelayBackoffType.Exponential,
    UseJitter = true
};

var slowRetries = new RetryStrategyOptions
{
    MaxRetryAttempts = 2,
    Delay = TimeSpan.FromSeconds(5),
    BackoffType = DelayBackoffType.Constant
};

var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(slowRetries)   // Outer layer
    .AddRetry(quickRetries)  // Inner layer
    .Build();

Retry with Maximum Duration

var options = new RetryStrategyOptions
{
    MaxRetryAttempts = int.MaxValue,
    Delay = TimeSpan.FromSeconds(2),
    MaxDelay = TimeSpan.FromMinutes(1)
};

var timeout = new ResiliencePipelineBuilder()
    .AddRetry(options)
    .AddTimeout(TimeSpan.FromMinutes(5)) // Overall timeout
    .Build();

Examples

HTTP Client with Retry

var httpClient = new HttpClient();

var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
    {
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .Handle<HttpRequestException>()
            .HandleResult(r => !r.IsSuccessStatusCode && r.StatusCode != HttpStatusCode.BadRequest),
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromSeconds(1),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true
    })
    .Build();

var response = await pipeline.ExecuteAsync(async ct =>
{
    return await httpClient.GetAsync("https://api.example.com/data", ct);
}, cancellationToken);

Database Connection with Retry

var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        ShouldHandle = new PredicateBuilder()
            .Handle<SqlException>(ex => ex.IsTransient)
            .Handle<TimeoutException>(),
        MaxRetryAttempts = 5,
        Delay = TimeSpan.FromSeconds(2),
        BackoffType = DelayBackoffType.Exponential,
        OnRetry = args =>
        {
            Console.WriteLine($"Database connection failed. Retry {args.AttemptNumber}");
            return default;
        }
    })
    .Build();

await pipeline.ExecuteAsync(async ct =>
{
    await using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync(ct);
    // Execute query
}, cancellationToken);
The retry strategy rethrows the final exception back to the calling code. You should handle this exception appropriately.
Yes, set MaxRetryAttempts = int.MaxValue. However, this is generally not recommended. Consider using a circuit breaker or implementing a maximum time limit instead.
Use the OnRetry callback to log retry attempts. You can also enable Polly telemetry for comprehensive observability.

Build docs developers (and LLMs) love