Em sistemas distribuídos, falhas parciais são inevitáveis: o banco de dados trava por 500ms, um microsserviço externo retorna 503, uma API de terceiro tem spike de latência. Sem resiliência, uma falha em um ponto se propaga em cascata e derruba o sistema inteiro. Polly é a biblioteca de resiliência padrão do ecossistema .NET — e no .NET 8, a Microsoft empacotou as melhores práticas em Microsoft.Extensions.Http.Resilience.
Polly v8 + Microsoft.Extensions.Http.Resilience
dotnet add package Microsoft.Extensions.Http.Resilience dotnet add package Polly.Extensions // Polly v8 já vem incluso no .NET 8 via Microsoft.Extensions.Http.Resilience// Program.cs — pipeline de resiliência padrão (cobre 80% dos casos): builder.Services .AddHttpClient(“produtos-api”, client => client.BaseAddress = new Uri(“https://produtos-service/”)) .AddStandardResilienceHandler(options => { // Retry: tenta 3x com backoff exponencial + jitter options.Retry.MaxRetryAttempts = 3; options.Retry.BackoffType = DelayBackoffType.Exponential; options.Retry.UseJitter = true; // aleatoriza o delay — evita thundering herd options.Retry.Delay = TimeSpan.FromMilliseconds(200);
// Circuit breaker: abre após 50% de falhas em 10s options.CircuitBreaker.FailureRatio = 0.5; options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10); options.CircuitBreaker.MinimumThroughput = 5; // mínimo de requisições para avaliar options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30); // tempo aberto // Timeout por tentativa: options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5); // Timeout total (inclui todos os retries): options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30); });
Políticas customizadas com ResiliencePipeline
Para casos específicos, monte o pipeline manualmente:
// Registrar um pipeline nomeado: builder.Services.AddResiliencePipeline("pagamentos", builder => { builder // 1. Timeout por tentativa (primeiro — cancela rapidamente) .AddTimeout(TimeSpan.FromSeconds(3))// 2. Retry com backoff exponencial e jitter .AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3, BackoffType = DelayBackoffType.Exponential, UseJitter = true, Delay = TimeSpan.FromMilliseconds(300), // Só tenta novamente em falhas transientes (não em 4xx): ShouldHandle = new PredicateBuilder() .Handle<HttpRequestException>() .Handle<TimeoutRejectedException>() .HandleResult<HttpResponseMessage>(r => r.StatusCode is HttpStatusCode.ServiceUnavailable or HttpStatusCode.TooManyRequests or HttpStatusCode.GatewayTimeout), OnRetry = args => { logger.LogWarning( "Retry {Attempt} para pagamentos. Delay: {Delay}ms. Motivo: {Outcome}", args.AttemptNumber, args.RetryDelay.TotalMilliseconds, args.Outcome.Exception?.Message ?? args.Outcome.Result?.StatusCode.ToString()); return ValueTask.CompletedTask; } }) // 3. Circuit breaker (depois do retry) .AddCircuitBreaker(new CircuitBreakerStrategyOptions { FailureRatio = 0.6, SamplingDuration = TimeSpan.FromSeconds(15), MinimumThroughput = 10, BreakDuration = TimeSpan.FromSeconds(45), OnOpened = args => { logger.LogError("Circuit breaker ABERTO para pagamentos. Duração: {Duration}s", args.BreakDuration.TotalSeconds); return ValueTask.CompletedTask; }, OnClosed = args => { logger.LogInformation("Circuit breaker FECHADO para pagamentos. Funcionando normalmente."); return ValueTask.CompletedTask; }, }) // 4. Fallback — resposta padrão quando tudo falha .AddFallback(new FallbackStrategyOptions<HttpResponseMessage> { ShouldHandle = new PredicateBuilder<HttpResponseMessage>() .Handle<Exception>(), FallbackAction = args => { logger.LogError(args.Outcome.Exception, "Fallback ativado para pagamentos"); // Retorna resposta de fallback (ex: enfileira para processar depois) var response = new HttpResponseMessage(HttpStatusCode.Accepted) { Content = new StringContent( """{"status":"enfileirado","mensagem":"Pagamento será processado em breve"}""", Encoding.UTF8, "application/json") }; return ValueTask.FromResult(response); } });
});
Usando o pipeline em serviços
public class PagamentoService { private readonly ResiliencePipeline _pipeline; private readonly HttpClient _httpClient;public PagamentoService(ResiliencePipelineProvider<string> pipelineProvider, IHttpClientFactory clientFactory) { _pipeline = pipelineProvider.GetPipeline("pagamentos"); _httpClient = clientFactory.CreateClient("pagamentos-api"); } public async Task<PagamentoResultado> ProcessarAsync(PagamentoRequest request, CancellationToken ct) { return await _pipeline.ExecuteAsync(async token => { var response = await _httpClient.PostAsJsonAsync("/pagamentos", request, token); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<PagamentoResultado>(token) ?? throw new InvalidOperationException("Resposta vazia"); }, ct); }
}
Bulkhead: isolamento de recursos
Bulkhead evita que uma dependência lenta consuma todas as threads da aplicação, derrubando outros fluxos:
builder.Services.AddResiliencePipeline("relatorios-pesados", builder =>
{
builder
// Limita chamadas concorrentes a APIs lentas
.AddConcurrencyLimiter(new ConcurrencyLimiterStrategyOptions
{
MaxConcurrentExecutions = 5, // máximo 5 chamadas simultâneas
QueuedTasksLimit = 10, // fila de espera de até 10
OnRejected = args =>
{
logger.LogWarning("Bulkhead cheio para relatórios. Request rejeitado.");
return ValueTask.CompletedTask;
}
})
.AddTimeout(TimeSpan.FromSeconds(30));
});
Hedging: dispara múltiplas requisições em paralelo
Hedging é útil quando a latência importa mais que o custo: dispara a segunda requisição se a primeira não responder dentro do prazo:
builder.Services.AddResiliencePipeline<string, ProdutoDto>("busca-rapida", builder =>
{
builder.AddHedging(new HedgingStrategyOptions<ProdutoDto>
{
// Se não responder em 200ms, dispara uma segunda tentativa em paralelo
Delay = TimeSpan.FromMilliseconds(200),
MaxHedgedAttempts = 2, // até 2 tentativas paralelas (3 no total)
ActionGenerator = args =>
{
// Pode redirecionar para endpoint diferente (cache, réplica read-only)
return () => args.Callback(args.ActionContext);
}
});
});
Monitoramento do estado do circuit breaker
// Endpoint de saúde que expõe o estado dos circuit breakers: app.MapGet("/health/resilience", (ResiliencePipelineProvider<string> provider) => { var pipelines = new[] { "pagamentos", "produtos", "relatorios-pesados" }; var status = pipelines.Select(name => { try { var pipeline = provider.GetPipeline(name); return new { nome = name, status = "ok" }; } catch { return new { nome = name, status = "degradado" }; } }); return Results.Ok(status); });
// Com OpenTelemetry — métricas automáticas de Polly: builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { metrics.AddMeter(“Polly”); // expõe métricas no Prometheus/Grafana // polly.resilience.pipeline.open_state (circuit breaker aberto) // polly.resilience.pipeline.attempt.duration (latência por tentativa) // polly.resilience.pipeline.total_duration (latência total incluindo retries) });
Configurando resiliência por variável de ambiente (sem recompilar)
// appsettings.json: { "Resiliencia": { "Pagamentos": { "MaxRetryAttempts": 3, "BreakDuration": 30, "AttemptTimeoutSeconds": 5 } } }// Program.cs — lê configuração: var resilConfig = builder.Configuration.GetSection(“Resiliencia:Pagamentos”);
builder.Services.AddResiliencePipeline(“pagamentos”, pipelineBuilder => { pipelineBuilder.AddRetry(new RetryStrategyOptions { MaxRetryAttempts = resilConfig.GetValue<int>(“MaxRetryAttempts”, 3), BackoffType = DelayBackoffType.Exponential, UseJitter = true, }) .AddCircuitBreaker(new CircuitBreakerStrategyOptions { BreakDuration = TimeSpan.FromSeconds( resilConfig.GetValue<int>(“BreakDuration”, 30)), }) .AddTimeout(TimeSpan.FromSeconds( resilConfig.GetValue<int>(“AttemptTimeoutSeconds”, 5))); });
Checklist de resiliência em produção
- Timeout em toda chamada HTTP externa — sem timeout, uma dependência travada trava sua thread para sempre
- Retry apenas em falhas transientes — nunca retente em
400 Bad Requestou409 Conflict - Jitter no retry — evita thundering herd (todos retentando ao mesmo tempo)
- Circuit breaker para cada dependência externa — falha isolada não deve virar falha total
- Bulkhead para operações lentas — relatórios, exports, APIs externas lentas
- Fallback com resposta degradada — melhor retornar algo do que erro 500
- Métricas de Polly no Prometheus — monitore taxa de retry e estado do circuit breaker
Conclusão
Resiliência não é optional em sistemas distribuídos — é requisito. O AddStandardResilienceHandler do .NET 8 entrega retry, circuit breaker e timeout com uma linha, e o Polly v8 permite customizar cada política para casos específicos. O resultado é um sistema que degrada graciosamente em vez de colapsar quando uma dependência fica instável.
Se você está enfrentando falhas em cascata ou latência imprevisível em microsserviços .NET, a Neryx pode ajudar com a análise e implementação de resiliência. Consultoria inicial gratuita.
Leitura complementar: