.NET Polly Resiliência Microsserviços C#

Resiliência em .NET com Polly: retry, circuit breaker, timeout e fallback na prática

Como implementar resiliência em APIs .NET com Polly e Microsoft.Extensions.Http.Resilience: políticas de retry com backoff exponencial, circuit breaker.

N
Neryx Digital Architects
18 de janeiro de 2026
12 min de leitura
210 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

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 Request ou 409 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:

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.