Skip to main content

How to Implement Retry Policies in Polly

A Polly retry policy automatically repeats a failed operation using configurable delays, exponential backoff, and jitter to spread load. Retry policies are the simplest and most effective tool for handling transient failures like network timeouts or brief service unavailability. This guide teaches you how to configure them in real .NET code.

How Retry Policies Work in Polly

A retry policy intercepts a failed operation, waits a configured time, and tries again. You define:

  • Retry count: How many times to retry (e.g., 3 total attempts = 2 retries).
  • Delay strategy: Fixed delay, linear backoff, or exponential backoff between retries.
  • Retry conditions: Which exceptions or status codes trigger a retry (e.g., HttpRequestException, 503 Service Unavailable, but not 401 Unauthorized).

Polly retries are synchronous or asynchronous, applied inline to your code, and compose with other policies.

Basic Retry: Fixed Delay

The simplest retry waits a fixed time between attempts:

using Polly;
using System;
using System.Net.Http;

var policy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutException>()
.WaitAndRetry(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(500),
onRetry: (outcome, span, attemptCount, context) =>
{
Console.WriteLine($"Retry {attemptCount} after {span.TotalMilliseconds}ms");
}
);

var result = policy.Execute(() =>
{
using var client = new HttpClient();
return client.GetStringAsync("https://api.example.com/data").Result;
});

Console.WriteLine($"Success: {result.Length} chars");

How it works:

  • Handle<HttpRequestException>() catches HTTP errors.
  • .Or<TimeoutException>() also retries on timeout.
  • WaitAndRetry(retryCount: 3, ...) retries up to 3 times, waiting 500 ms each time.
  • onRetry callback logs each retry attempt.
  • .Execute() runs the operation, retrying on failure.

Real-world scenario: An external API is briefly unavailable due to a deploy. The first request times out; Polly waits 500 ms and retries; the API is back online, and the second attempt succeeds.

Exponential Backoff with Jitter

Exponential backoff doubles the delay each retry, preventing thundering herd (many clients retrying simultaneously). Jitter adds randomness to stagger retries:

using Polly;
using System;
using System.Net.Http;

var policy = Policy
.Handle<HttpRequestException>()
.WaitAndRetry(
retryCount: 4,
sleepDurationProvider: attempt =>
{
var exponentialBackoff = TimeSpan.FromMilliseconds(
Math.Pow(2, attempt) * 100
);
var jitter = TimeSpan.FromMilliseconds(
new Random().Next(0, 50)
);
return exponentialBackoff + jitter;
},
onRetry: (outcome, span, attemptCount, context) =>
{
Console.WriteLine($"Attempt {attemptCount} failed. Waiting {span.TotalMilliseconds:F0}ms before retry.");
}
);

using var client = new HttpClient();
var result = policy.Execute(() =>
client.GetStringAsync("https://api.example.com/status").Result
);

Backoff schedule:

  • Attempt 1: 100 ms + jitter (0-50 ms) = 100-150 ms
  • Attempt 2: 200 ms + jitter = 200-250 ms
  • Attempt 3: 400 ms + jitter = 400-450 ms
  • Attempt 4: 800 ms + jitter = 800-850 ms

Why jitter? Without jitter, all clients retry at the same time, overwhelming a recovering service. Jitter spreads them out.

Async Retry with Polly

For async operations (which are standard in modern .NET), use WaitAndRetryAsync:

using Polly;
using System;
using System.Net.Http;
using System.Threading.Tasks;

var policy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt =>
TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 100),
onRetryAsync: async (outcome, span, attemptCount, context) =>
{
Console.WriteLine($"Async retry {attemptCount} after {span.TotalMilliseconds}ms");
await Task.CompletedTask;
}
);

using var client = new HttpClient();
var result = await policy.ExecuteAsync(async () =>
await client.GetStringAsync("https://api.example.com/data")
);

Console.WriteLine($"Result: {result}");

Key difference: WaitAndRetryAsync is non-blocking; it doesn't consume a thread while waiting.

Conditional Retry Based on Status Code

Polly can retry based on response status codes, not just exceptions. This is useful for APIs that return 503 (Service Unavailable) or 429 (Too Many Requests):

using Polly;
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

var policy = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r =>
r.StatusCode == HttpStatusCode.ServiceUnavailable ||
r.StatusCode == HttpStatusCode.TooManyRequests ||
r.StatusCode == HttpStatusCode.GatewayTimeout
)
.WaitAndRetryAsync<HttpResponseMessage>(
retryCount: 3,
sleepDurationProvider: attempt =>
TimeSpan.FromSeconds(Math.Pow(2, attempt)),
onRetryAsync: async (outcome, span, attemptCount, context) =>
{
var statusCode = outcome.Result?.StatusCode ?? 0;
Console.WriteLine($"Retry {attemptCount} after {span.TotalSeconds}s. Status: {statusCode}");
await Task.CompletedTask;
}
);

using var client = new HttpClient();
var response = await policy.ExecuteAsync(async () =>
await client.GetAsync("https://api.example.com/data")
);

if (response.IsSuccessStatusCode)
Console.WriteLine("Success: " + await response.Content.ReadAsStringAsync());
else
Console.WriteLine($"Final failure: {response.StatusCode}");

When to use this: REST APIs that return rate-limit or service-unavailable responses instead of throwing exceptions.

Key Takeaways

  • Retry policies automatically repeat failed operations with configurable delays.
  • Fixed delay is simple; exponential backoff prevents thundering herd and scales well for distributed systems.
  • Jitter randomizes retry timing, reducing collision when multiple clients retry simultaneously.
  • Async retry (WaitAndRetryAsync) is non-blocking and standard in .NET; prefer it over sync.
  • Status code conditions let you retry on specific HTTP responses (503, 429) without exceptions.

Frequently Asked Questions

How many retries should I configure?

For transient failures (timeouts, brief unavailability), 3 retries with exponential backoff usually suffice. For heavily rate-limited APIs, 5-6 retries with longer backoff (several seconds) may be needed. Measure your baseline failure rate and choose retries that give 99% success without excessive latency.

Can exponential backoff cause too much delay?

Yes. If you exceed several seconds total delay, user experience suffers. Cap the maximum wait time using maxDelay parameter in Polly or calculate max total delay = retries × max-delay. For user-facing APIs, keep total retry time under 5-10 seconds.

Should I retry all exception types?

No. Retry only transient failures: HttpRequestException, TimeoutException, IOException. Never retry ArgumentException or NullReferenceException—these are bugs, not transient failures. Retrying them wastes time and masks the real problem.

How do I test retry logic?

Use a mock that fails N times, then succeeds. Polly includes Policy.Timeout(Timeout.InfiniteTimeSpan) for testing. Write unit tests that verify retry count and backoff timing using onRetry callbacks or counters.

Further Reading