Como cortei 40% do tempo de resposta no .NET 8 em produção
Do diagnóstico de gargalos ao ajuste fino de P95. Como cache inteligente em memória, prevenção de cache stampede, middleware customizado de profiling e health checks honestos transformaram a estabilidade de uma API corporativa sob carga pesada.
Ouvimos com frequência que '.NET já é rápido por padrão'. É verdade: a máquina virtual do .NET 8 é uma das mais velozes do mercado. Mas quando seu sistema atende milhares de requisições simultâneas em um App Service na nuvem, código rápido não salva uma arquitetura que faz viagens desnecessárias ao banco de dados ou sofre de bloqueios de concorrência.
Na CordenaAI, enfrentávamos picos de tráfego onde o tempo de resposta do percentil 95 (P95) saltava de 180ms para quase 900ms. A primeira reação de quem não mede é pedir mais hardware: dobrar o tamanho do App Service e pagar o dobro na fatura da Azure. Decidi seguir o caminho da engenharia: instrumentar cada milissegundo, entender onde o tempo estava sendo desperdiçado e cortar pela raiz.
1. MemoryCache cirúrgico e o fantasma da Cache Stampede
O maior vilão da nossa latência era a busca repetida de tabelas de configuração, planos de clientes e metadados que mudavam pouquíssimas vezes por semana. Toda requisição batia no banco para buscar as mesmas 15 linhas. Implementar IMemoryCache foi o primeiro passo óbvio, mas ele trouxe um perigo invisível: a avalanche de cache (Cache Stampede).
O que acontece quando uma chave de cache com TTL de 10 minutos expira e você tem 400 requisições simultâneas chegando naquele exato segundo? Todas as 400 requisições descobrem que o cache está vazio e todas as 400 disparam a mesma query pesada contra o banco ao mesmo tempo. O banco de dados trava, o pool de conexões esgota e a aplicação cai.
A cura foi implementar um padrão de bloqueio de concorrência com SemaphoreSlim e double-check: a primeira requisição que descobre a expiração adquire a trava e busca os dados no banco; as outras 399 aguardam alguns milissegundos e leem o cache recém-preenchido. O banco nunca mais recebeu mais de uma requisição simultânea para a mesma chave expirada.
Otimização sem medição e sem proteção de concorrência é apenas superstição rápida.
2. Query Performance Middleware: medindo a dor em tempo real
Sem métricas em tempo real, você otimiza o que acha que está lento, não o que realmente está travando os clientes. Escrevi um Query Performance Middleware leve que intercepta cada request no pipeline HTTP do ASP.NET Core, calcula o tempo de execução do pipeline interno e emite alertas estruturados no Application Insights sempre que um endpoint estoura 250ms.
Esse middleware revelou surpresas inacreditáveis: um endpoint secundário de consulta de status estava serializando grafos gigantescos de entidades do Entity Framework com dezenas de relacionamentos desnecessários. Trocar as consultas por projeções explícitas com AsNoTracking() e Select() direto em DTOs enxutos eliminou 70% da alocação de memória no Garbage Collector.
3. Health checks honestos: pare de mentir para o balanceador
O erro mais comum que encontro em backends modernos é o endpoint de saúde que apenas devolve 'HTTP 200 OK' de forma estática. Se o banco de dados cair ou a rede do Redis travar, o pod continua dizendo ao Azure Load Balancer que está saudável e pronto para receber tráfego. O resultado? Usuários recebendo erro 500 enquanto o dashboard da nuvem brilha em verde.
Implementei Health Checks customizados com timeouts rigorosos (2 segundos) para cada dependência vital. Se o banco falhar, o serviço falha alto imediatamente, o balanceador redireciona as requisições para réplicas saudáveis e o alarme dispara. Um sistema resiliente não esconde falhas; ele as expõe com rapidez para proteger o usuário.