Isaac.

Polly Resilience Patterns

Implement resilience with retry, circuit breaker, and bulkhead patterns.

By EMEPublished: February 20, 2025
pollyresiliencecircuit breakerretryfault tolerance

A Simple Analogy

Polly is like insurance for your API calls. When something fails, Polly automatically retries, stops calling broken services, and isolates failures to prevent cascading problems.


Why Polly?

  • Resilience: Auto-recover from transient failures
  • Circuit breaker: Stop calling failing services
  • Retry logic: With exponential backoff
  • Bulkhead: Isolate resource consumption
  • Fallback: Provide alternative responses

Retry Policy

var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .Or<TimeoutException>()
    .WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: attempt => 
            TimeSpan.FromSeconds(Math.Pow(2, attempt)), // 1s, 2s, 4s
        onRetry: (outcome, timespan, retryCount, context) =>
        {
            logger.LogWarning(
                $"Retry {retryCount} after {timespan.TotalSeconds}s. Reason: {outcome.Exception?.Message}");
        });

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

Circuit Breaker

var circuitBreakerPolicy = Policy
    .Handle<HttpRequestException>()
    .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    .CircuitBreakerAsync(
        handledEventsAllowedBeforeBreaking: 5,
        durationOfBreak: TimeSpan.FromSeconds(30),
        onBreak: (outcome, timespan) =>
        {
            logger.LogError($"Circuit breaker opened for {timespan.TotalSeconds}s");
        },
        onReset: () =>
        {
            logger.LogInformation("Circuit breaker reset");
        });

try
{
    var response = await circuitBreakerPolicy.ExecuteAsync(async () =>
    {
        return await httpClient.GetAsync("https://failing-api.com/data");
    });
}
catch (BrokenCircuitException)
{
    logger.LogError("Circuit breaker is open. Service is down.");
}

Bulkhead Isolation

var bulkheadPolicy = Policy.BulkheadAsync(
    maxParallelization: 10,
    maxQueuingActions: 20,
    onBulkheadRejectedExecutionAsync: context =>
    {
        logger.LogWarning("Bulkhead queue is full. Request rejected.");
        return Task.CompletedTask;
    });

try
{
    await bulkheadPolicy.ExecuteAsync(async () =>
    {
        await Task.Delay(1000); // Simulates work
    });
}
catch (BulkheadRejectedException)
{
    // Handle rejection
}

Combined Policies (Wrap)

// Retry -> Circuit Breaker -> Bulkhead
var policy = Policy.WrapAsync(
    retryPolicy,
    circuitBreakerPolicy,
    bulkheadPolicy);

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

Fallback Policy

var fallbackPolicy = Policy<HttpResponseMessage>
    .Handle<HttpRequestException>()
    .OrResult(r => !r.IsSuccessStatusCode)
    .FallbackAsync(
        fallbackAction: async () =>
        {
            logger.LogWarning("Using fallback response from cache");
            var cached = await cache.GetAsync("api-response");
            return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
            {
                Content = new StringContent(cached)
            };
        },
        onFallbackAsync: (outcome, context) =>
        {
            logger.LogError($"Fallback triggered: {outcome.Exception?.Message}");
            return Task.CompletedTask;
        });

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

HttpClientFactory Integration

builder.Services.AddHttpClient<IPaymentClient, PaymentClient>()
    .AddTransientHttpErrorPolicy()
    .WaitAndRetryAsync(3, retryAttempt =>
        TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

builder.Services.AddHttpClient<IOrderClient, OrderClient>()
    .AddTransientHttpErrorPolicy()
    .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));

builder.Services.AddHttpClient<IUserClient, UserClient>()
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

Advanced: Async Bulkhead

var asyncBulkheadPolicy = Policy.BulkheadAsync(
    maxParallelization: 10,
    maxQueuingActions: 20);

var database = new AsyncBulkheadPolicy();
var externalApi = new AsyncBulkheadPolicy();

// Keep database and API calls isolated
var dbPolicy = asyncBulkheadPolicy;
var apiPolicy = asyncBulkheadPolicy;

// Even if API is overloaded, database calls proceed

Best Practices

  1. Use defaults: Start with retry and circuit breaker
  2. Monitor policies: Track rejections and failures
  3. Set realistic timeouts: Avoid cascading waits
  4. Test failure scenarios: Verify fallback works
  5. Document policies: Explain why each exists

Related Concepts

  • Hystrix (Java equivalent)
  • Service mesh patterns
  • Distributed tracing
  • Health checks

Summary

Polly provides resilience patterns for distributed systems. Combine retry, circuit breaker, and bulkhead policies to build systems that gracefully handle failures and failures.