O Polly é uma das bibliotecas mais usadas no ecossistema .NET — e por boas razões. Mas o Polly "clássico" (v7) tem uma API que acumulou complexidade ao longo dos anos. Com o .NET 8, a Microsoft co-desenvolveu com a equipe do Polly uma nova versão (v8) e integrou ao SDK como Microsoft.Extensions.Resilience, com uma API mais moderna, composição mais clara, e telemetria automática com OpenTelemetry.
Se você tem código com Policy.Handle<HttpRequestException>().WaitAndRetryAsync(...), este guia mostra como migrar para a abordagem mais moderna — e o que ganhar com isso.
Instalação e conceitos centrais
dotnet add package Microsoft.Extensions.Resilience
dotnet add package Microsoft.Extensions.Http.Resilience # para HttpClient
O conceito central do Polly v8 é o ResiliencePipeline: uma sequência ordenada de estratégias de resiliência. Cada chamada passa por todas as estratégias na ordem definida. A ordem importa: um timeout externo ao retry tem comportamento diferente de um timeout interno ao retry.
ResiliencePipeline: composição explícita
// Configuração manual de um pipeline — máximo controle
var pipeline = new ResiliencePipelineBuilder()
// 1. Timeout total da operação (mais externo)
.AddTimeout(TimeSpan.FromSeconds(10))
// 2. Retry com backoff exponencial e jitter
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true, // evita thundering herd
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.Handle<TimeoutRejectedException>()
.HandleResult<HttpResponseMessage>(r =>
(int)r.StatusCode >= 500 || r.StatusCode == HttpStatusCode.TooManyRequests),
OnRetry = args =>
{
Console.WriteLine(
$"Retry {args.AttemptNumber} após {args.RetryDelay.TotalMilliseconds}ms " +
$"devido a: {args.Outcome.Exception?.Message ?? args.Outcome.Result?.StatusCode.ToString()}");
return ValueTask.CompletedTask;
}
})
// 3. Circuit Breaker — abre após falhas consecutivas
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5, // abre se 50% das chamadas falharem
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10, // mínimo de chamadas para calcular o ratio
BreakDuration = TimeSpan.FromSeconds(15), // tempo aberto antes de half-open
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.HandleResult<HttpResponseMessage>(r => (int)r.StatusCode >= 500),
OnOpened = args =>
{
Console.WriteLine(
$"Circuit Breaker ABERTO por {args.BreakDuration.TotalSeconds}s. " +
$"Causa: {args.Outcome.Exception?.Message ?? args.Outcome.Result?.StatusCode.ToString()}");
return ValueTask.CompletedTask;
},
OnClosed = args =>
{
Console.WriteLine("Circuit Breaker FECHADO — serviço recuperado");
return ValueTask.CompletedTask;
}
})
// 4. Timeout por tentativa individual (mais interno)
.AddTimeout(TimeSpan.FromSeconds(3))
.Build();
// Execução
var response = await pipeline.ExecuteAsync(
async ct => await httpClient.GetAsync("/api/orders", ct),
cancellationToken);
AddStandardResilienceHandler: o padrão recomendado para HttpClient
// Program.cs — configuração mais limpa e integrada ao DI
builder.Services.AddHttpClient<IOrderServiceClient, OrderServiceClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Services:OrderService:BaseUrl"]!);
})
// Pipeline padrão com boas configurações de produção:
// Total Timeout → Retry (3x, exponential) → Circuit Breaker → Attempt Timeout
.AddStandardResilienceHandler(options =>
{
// Customiza apenas o que difere do padrão
options.Retry.MaxRetryAttempts = 4;
options.Retry.Delay = TimeSpan.FromMilliseconds(300);
options.CircuitBreaker.FailureRatio = 0.4;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(20);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(15);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(4);
});
// Valores padrão do AddStandardResilienceHandler (quando não customizado):
// TotalTimeout: 30s
// Retry: 3 tentativas, exponential backoff com jitter, delay inicial 2s
// CircuitBreaker: 10% failure ratio, 30s sampling, 5 chamadas mínimas, 30s break
// AttemptTimeout: 10s
Hedging: chamadas paralelas para reduzir latência de cauda
// Hedging é a estratégia menos conhecida e mais poderosa para latência de cauda.
// Dispara uma segunda chamada se a primeira não responde dentro do delay.
// Retorna o resultado de quem responder primeiro — a mais lenta é cancelada.
builder.Services.AddHttpClient<IProductCatalogClient, ProductCatalogClient>()
.AddResilienceHandler("product-catalog", pipeline =>
{
// Hedging: se não responder em 500ms, dispara outra chamada em paralelo
pipeline.AddHedging(new HedgingStrategyOptions<HttpResponseMessage>
{
MaxHedgedAttempts = 2, // máximo de chamadas paralelas
Delay = TimeSpan.FromMilliseconds(500), // tempo antes de hedging
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => r.StatusCode == HttpStatusCode.ServiceUnavailable),
ActionGenerator = static args =>
{
// Mesma requisição para a mesma URL (ou URL alternativa)
return () => args.Callback(args.ActionContext);
}
});
pipeline.AddTimeout(TimeSpan.FromSeconds(5));
});
Telemetria automática com OpenTelemetry
// O Microsoft.Extensions.Resilience emite métricas e traces automaticamente
// via OpenTelemetry — sem configuração adicional.
// Program.cs — habilita exportação para o coletor de telemetria
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
// Métricas automáticas do Resilience:
// resilience.polly.retry.duration
// resilience.polly.circuit_breaker.state
// resilience.polly.hedging.attempts
.AddMeter("Microsoft.Extensions.Resilience");
})
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
// Spans para cada tentativa de retry e transição do circuit breaker
.AddSource("Microsoft.Extensions.Resilience");
});
// Métricas disponíveis no Prometheus/Grafana sem nenhum código adicional:
// - Número de retries por segundo
// - Estado atual dos circuit breakers (open/closed/half-open)
// - Latência de cada tentativa
// - Taxa de sucesso/falha por pipeline
Resiliência por chave: pipelines diferentes por serviço dependente
// Quando você tem múltiplos serviços com SLAs diferentes,
// configure pipelines distintos via keyed services
builder.Services.AddResiliencePipeline("payment-gateway", (builder, context) =>
{
// Gateway de pagamento: mais conservador, menor timeout, sem hedging
builder
.AddTimeout(TimeSpan.FromSeconds(5))
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 2,
Delay = TimeSpan.FromMilliseconds(500),
// Não retentar em 4xx — são erros do caller, não do serviço
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.HandleResult<HttpResponseMessage>(r => (int)r.StatusCode >= 500)
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
BreakDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 5
});
});
builder.Services.AddResiliencePipeline("internal-cache", (builder, context) =>
{
// Cache interno: agressivo, aceita degradação
builder
.AddTimeout(TimeSpan.FromMilliseconds(100)) // falha rápido
.AddFallback(new FallbackStrategyOptions<CacheResult>
{
FallbackAction = _ => ValueTask.FromResult(CacheResult.Miss()),
ShouldHandle = new PredicateBuilder<CacheResult>()
.Handle<TimeoutRejectedException>()
.Handle<Exception>()
});
});
// Injeção via IResiliencePipelineProvider
public class OrderService
{
private readonly ResiliencePipeline _paymentPipeline;
public OrderService(IResiliencePipelineProvider<string> provider)
{
_paymentPipeline = provider.GetPipeline("payment-gateway");
}
public async Task ProcessPaymentAsync(PaymentRequest request, CancellationToken ct)
{
await _paymentPipeline.ExecuteAsync(async token =>
{
await _paymentGateway.ChargeAsync(request, token);
}, ct);
}
}
Migrando do Polly v7 para v8
| Polly v7 (legado) | Polly v8 / Microsoft.Extensions.Resilience |
|---|---|
Policy.Handle<Ex>().WaitAndRetryAsync(3, ...) |
builder.AddRetry(new RetryStrategyOptions { ... }) |
Policy.WrapAsync(retry, cb, timeout) |
new ResiliencePipelineBuilder().AddRetry().AddCB().AddTimeout() |
IAsyncPolicy<T> |
ResiliencePipeline<T> |
AddPolicyHandler(policy) |
AddStandardResilienceHandler() ou AddResilienceHandler() |
| Sem telemetria nativa | OpenTelemetry automático (métricas + traces) |
Context manual para logging |
Telemetria automática via TelemetryOptions |
A migração não precisa ser feita de uma vez. O Polly v8 é backward-compatible com v7 para a maioria dos cenários. A estratégia mais segura é migrar um HttpClient por vez, começando pelo que tem mais volume (para validar em produção com dados reais) ou pelo mais crítico (gateway de pagamento, serviço de identidade).
Resiliência bem implementada é a diferença entre um sistema que degrada graciosamente e um que cascateia falhas. Se você quer revisar e fortalecer a estratégia de resiliência do seu sistema .NET, a Neryx pode ajudar.