Por que você precisa de Backpressure
Existe um mito comum na engenharia de software moderna: "Se o sistema ficar lento, basta adicionar mais instâncias". Embora o Auto-Scaling horizontal seja importante, ele não resolve o desequilíbrio fundamental de fluxo. Se um serviço produtor (o "firehose") envia dados 10x mais rápido do que o serviço consumidor consegue processar, adicionar mais consumidores pode apenas deslocar o gargalo para o banco de dados (o próximo componente da cadeia arquitetural), derrubando toda a sua infraestrutura de persistência. A solução para isso não é (apenas) escalar, é controlar o fluxo. É implementar Backpressure (Contrapressão).
O Que é Backpressure?
Backpressure é o mecanismo pelo qual um componente sobrecarregado sinaliza ao componente anterior (upstream) que ele precisa "diminuir o ritmo". Em vez de o consumidor tentar engolir tudo até estourar a memória (OutOfMemoryException) ou travar a CPU, ele recusa novas tarefas ou impõe uma espera (filas com controle de processamento são geralmente o caminho que adotamos aqui).
Nota Rápida: Diferente do Circuit Breaker, que abre o circuito após falhas, o Backpressure atua preventivamente para evitar a falha por exaustão.
Implementando Backpressure "Na Prática" com C#
Vamos sair da teoria. Como um serviço .NET (Consumidor) avisa que está cheio? O padrão mais simples e eficaz em APIs REST é o Throttling Concorrente. Se o serviço só aguenta processar 50 relatórios pesados simultaneamente, a 51ª requisição deve ser rejeitada imediatamente com um sinal claro: HTTP 429 Too Many Requests (já falei sobre esse erro e como lidar com ele em algum artigo ai no blog). Isso transfere a responsabilidade para o cliente (Produtor), que deve esperar antes de tentar novamente. Vamos ver uma implementação limpa usando SemaphoreSlim para controlar a concorrência em um endpoint crítico.
using Microsoft.AspNetCore.Mvc;
using System.Threading;
[ApiController]
[Route("api/relatorios")]
public class RelatoriosController : ControllerBase
{
// Define a capacidade máxima de processamento simultâneo.
// Em um cenário real, isso viria de uma configuração (appsettings).
private static readonly SemaphoreSlim _throttle = new SemaphoreSlim(10, 10);
[HttpPost("processar-lote")]
public async Task<IActionResult> ProcessarLoteComplexo([FromBody] PedidoRelatorio pedido)
{
// Tenta entrar no semáforo. Se estiver cheio (0), retorna false imediatamente (Wait(0)).
// Isso é o Backpressure em ação: rejeição rápida.
if (!await _throttle.WaitAsync(0))
{
// O Sinal: Avisamos o produtor que estamos cheios.
// O header 'Retry-After' sugere quando tentar de novo (ex: 5 segundos).
Response.Headers.Add("Retry-After", "5");
return StatusCode(429, "O sistema está sob alta carga. Tente novamente em 5 segundos.");
}
try
{
// --- INÍCIO DA ZONA CRÍTICA ---
// Aqui ocorre o processamento pesado (CPU Bound ou I/O lento)
await ProcessarLogicaPesada(pedido);
return Ok(new { status = "Processado com sucesso" });
// --- FIM DA ZONA CRÍTICA ---
}
finally
{
// Libera a vaga no semáforo para a próxima requisição
_throttle.Release();
}
}
private async Task ProcessarLogicaPesada(PedidoRelatorio pedido)
{
// Simulação de trabalho pesado
await Task.Delay(2000);
}
}
...
Claro que se estivermos falando de um serviço distribuído, limitar na memória não vai ajudar, teríamos de adotar Backpressure via Mensageria, consumindo mensagens no limite do consumidor, usar um cache distribuído ou, em alguns casos, até limitar o scale horizontal do consumidor para não estourar o limite (seria algo como: o limite é 50, tenho 5 instancias no máximo, então cada instancia limita a 10 threads na memória, mas isso pode ser meio restritivo pois a instancia recebe requisição de um loadbalancer, então poderíamos não estar aproveitando o limite ao máximo e ainda assim retornando erro 429)
Anatomia da Solução
O Guardião (SemaphoreSlim): Ele limita fisicamente quantas threads podem executar aquele bloco de código ao mesmo tempo. No exemplo, limitamos a 10. A Rejeição Rápida (WaitAsync(0)): Se as 10 vagas estiverem ocupadas, não enfileiramos a requisição na memória. Rejeitamos imediatamente. Isso economiza recursos vitais do servidor. O Sinal (HTTP 429): Este é o feedback explícito. O cliente recebe esse status e entende: "O servidor não caiu, ele apenas está ocupado. Devo esperar".
A Outra Metade: O Cliente
O Backpressure só funciona se o cliente respeitar o sinal. Um cliente resiliente, ao receber um 429, deve implementar uma estratégia de Exponential Backoff.
Se o cliente receber o 429 e tentar novamente no mesmo momento, pode criar um loop infinito, ele estará executando um ataque de negação de serviço (DDoS) contra você. Ele deve esperar 1s, depois 2s, depois 4s, etc.
Conclusão
Implementar Backpressure é aceitar que seu sistema tem limites físicos. Ao rejeitar tráfego excedente de forma controlada, você garante que as requisições aceitas sejam processadas com performance adequada e protege seu banco de dados de colapsar sob o efeito manada. É melhor negar serviço para 10% dos usuários e manter o sistema rápido para 90%, do que deixar o sistema lento ou indisponível para 100%.