Polly is a .NET resilience and transient-fault-handling library that allows developers to express concepts like retries, circuit breakers, and timeouts in a fluent and extensible way.

Let’s see Polly in action. Imagine you have a HttpClient that makes requests to a flaky external service. Without Polly, you might write something like this:

public async Task<string> GetDataFromExternalServiceAsync(HttpClient client)
{
    try
    {
        var response = await client.GetAsync("https://api.example.com/data");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
    catch (HttpRequestException ex)
    {
        // Log the error and potentially rethrow or return a default
        Console.WriteLine($"Request failed: {ex.Message}");
        throw;
    }
}

This code is brittle. If the external service is temporarily unavailable or slow, your application will immediately fail. Now, let’s introduce Polly to make this more robust.

First, you’ll need to install the Polly NuGet package:

dotnet add package Polly

Then, you can define resilience policies. A common scenario is retrying a request a few times if it fails with certain transient errors.

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

public class ExternalServiceCaller
{
    private readonly HttpClient _httpClient;
    private readonly AsyncRetryPolicy _retryPolicy;

    public ExternalServiceCaller(HttpClient httpClient)
    {
        _httpClient = httpClient;

        _retryPolicy = Policy
            .Handle<HttpRequestException>() // Handle specific exceptions
            .WaitAndRetryAsync(
                retryCount: 3, // Number of retries
                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // Exponential backoff
                onRetry: (exception, timeSpan, retryCount, context) =>
                {
                    Console.WriteLine($"Retrying request. Attempt {retryCount} after {timeSpan.TotalSeconds}s. Exception: {exception.Message}");
                });
    }

    public async Task<string> GetDataFromExternalServiceAsync()
    {
        return await _retryPolicy.ExecuteAsync(async () =>
        {
            var response = await _httpClient.GetAsync("https://api.example.com/data");
            response.EnsureSuccessStatusCode(); // Throws HttpRequestException on non-success status codes
            return await response.Content.ReadAsStringAsync();
        });
    }
}

In this example, we’ve defined a retryPolicy that will retry an HttpRequestException up to 3 times. The sleepDurationProvider uses exponential backoff (1s, 2s, 4s) to give the external service time to recover and avoid overwhelming it. The onRetry delegate allows you to log each retry attempt.

Polly’s power comes from its ability to combine policies. Let’s add a timeout to ensure requests don’t hang indefinitely.

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

public class ExternalServiceCaller
{
    private readonly HttpClient _httpClient;
    private readonly Polly.Policy _resiliencePolicy; // Combined policy

    public ExternalServiceCaller(HttpClient httpClient)
    {
        _httpClient = httpClient;

        var retryPolicy = Policy
            .Handle<HttpRequestException>()
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (exception, timeSpan, retryCount, context) =>
                {
                    Console.WriteLine($"Retrying request. Attempt {retryCount} after {timeSpan.TotalSeconds}s. Exception: {exception.Message}");
                });

        var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(10), onTimeoutAsync: async (context, timeSpan, task) =>
        {
            Console.WriteLine($"Request timed out after {timeSpan.TotalSeconds}s.");
            // You can throw a specific exception or handle it here
            throw new TimeoutException("External service request timed out.");
        });

        // Combine policies: Timeout first, then Retry
        _resiliencePolicy = timeoutPolicy.WrapAsync(retryPolicy);
    }

    public async Task<string> GetDataFromExternalServiceAsync()
    {
        return await _resiliencePolicy.ExecuteAsync(async () =>
        {
            var response = await _httpClient.GetAsync("https://api.example.com/data");
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        });
    }
}

Here, we’ve created a timeoutPolicy that will abort the operation if it takes longer than 10 seconds. We then WrapAsync the retryPolicy with the timeoutPolicy. This means that if the request times out, the timeoutPolicy will trigger. If the request fails with an HttpRequestException before the timeout, the retryPolicy will kick in. The order of wrapping matters: the outer policy acts first.

Another powerful resilience pattern is the Circuit Breaker. A circuit breaker prevents an application from repeatedly trying to execute an operation that’s likely to fail. After a certain number of failures, it "opens" the circuit, and subsequent calls fail immediately without attempting the operation. After a timeout, it "half-opens" the circuit, allowing a limited number of requests to pass through to test if the service has recovered.

using Polly;
using Polly.Retry;
using Polly.Timeout;
using Polly.CircuitBreaker;
using System.Net.Http;
using System.Threading.Tasks;

public class ExternalServiceCaller
{
    private readonly HttpClient _httpClient;
    private readonly Polly.Policy _resiliencePolicy;

    public ExternalServiceCaller(HttpClient httpClient)
    {
        _httpClient = httpClient;

        var circuitBreakerPolicy = new CircuitBreakerPolicyAsync(
            exceptionsAllowedBeforeBreaking: 5, // Number of exceptions before breaking
            durationOfBreak: TimeSpan.FromSeconds(30), // How long the circuit stays open
            onBreak: (exception, breakDelay) =>
            {
                Console.WriteLine($"Circuit broken for {breakDelay.TotalSeconds}s due to {exception.Message}");
            },
            onHalfOpen: () =>
            {
                Console.WriteLine("Circuit half-open. Allowing one request.");
            },
            onReset: () =>
            {
                Console.WriteLine("Circuit reset. Service is back online.");
            });

        var retryPolicy = Policy
            .Handle<HttpRequestException>()
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (exception, timeSpan, retryCount, context) =>
                {
                    Console.WriteLine($"Retrying request. Attempt {retryCount} after {timeSpan.TotalSeconds}s. Exception: {exception.Message}");
                });

        var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(10), onTimeoutAsync: async (context, timeSpan, task) =>
        {
            Console.WriteLine($"Request timed out after {timeSpan.TotalSeconds}s.");
            throw new TimeoutException("External service request timed out.");
        });

        // Order: Circuit Breaker -> Timeout -> Retry
        _resiliencePolicy = circuitBreakerPolicy.WrapAsync(timeoutPolicy.WrapAsync(retryPolicy));
    }

    public async Task<string> GetDataFromExternalServiceAsync()
    {
        return await _resiliencePolicy.ExecuteAsync(async () =>
        {
            var response = await _httpClient.GetAsync("https://api.example.com/data");
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        });
    }
}

In this layered approach, the circuitBreakerPolicy is the outermost. If 5 HttpRequestExceptions occur, the circuit breaks for 30 seconds. During that break, any calls to ExecuteAsync will immediately throw a CircuitBreakerOpenException. After 30 seconds, the circuit will half-open, allowing one request through. If that request succeeds, the circuit resets. If it fails, the circuit breaks again. The timeoutPolicy and retryPolicy are nested within. This means that even when the circuit is closed, requests are still subject to the 10-second timeout and the 3 retries.

Polly excels at making your C# applications more resilient by abstracting complex fault-handling logic into configurable policies. You can define custom exception handling, retry strategies, timeouts, and circuit breaker behavior with minimal code, directly within your application’s dependency injection setup or service configuration.

One of the more subtle but incredibly useful aspects of Polly is its ability to handle HttpResponseMessage directly, not just exceptions. For example, you can configure a retry policy to trigger based on specific HTTP status codes like 503 Service Unavailable, without needing to throw an exception first. This is configured using HandleResult.

var retryPolicyForServiceUnavailable = Policy
    .HandleResult<HttpResponseMessage>(r => r.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
    .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(5));

// Then execute your request and use this policy
await retryPolicyForServiceUnavailable.ExecuteAsync(async () =>
{
    var response = await _httpClient.GetAsync("...");
    // No need for response.EnsureSuccessStatusCode() if you're handling specific status codes manually
    return response;
});

This allows for much finer-grained control over when retries occur, directly reflecting the communication protocol’s error states rather than relying solely on exceptions.

The next step in building robust distributed systems with Polly is to explore advanced scenarios like bulkheads to limit the number of concurrent calls, or composing policies with OrResult to handle multiple types of failures with different strategies.

Want structured learning?

Take the full Csharp course →