.NET 10 para APIs de alto tráfego: o que realmente fica mais rápido — e o que continua sendo problema seu

.NET 10 para APIs de alto tráfego: o que realmente fica mais rápido — e o que continua sendo problema seu

7 de Abril de 2026

Olá pessoALL,

Toda vez que sai uma versão nova do .NET, a reação é previsível. Alguém posta um benchmark no Twitter mostrando ganhos de 20% em requests por segundo. O hype começa. E a expectativa de muita gente é sempre a mesma: atualizar o runtime, fazer deploy, e magicamente a API fica mais rápida.

Se você já atualizou um runtime esperando milagre e viu que quase nada mudou em produção, esse artigo é pra você.

Eu já passei por isso. Atualizei APIs para novas versões do .NET, vi benchmarks locais melhorarem, fiquei animado — e quando olhei o p95 de latência em produção, o número mal se mexeu. A causa? O gargalo nunca foi o framework. Era o meu próprio código. Queries pesadas, payloads retornando coisa que ninguém pedia. O runtime novo não ia salvar nada disso.

É isso que quero discutir aqui. O .NET 10 traz melhorias que importam. Mas essas melhorias só aparecem quando o seu código já está razoavelmente bem estruturado. O runtime não conserta código ruim. Essa é a frase que vai guiar todo esse artigo.

O Que o .NET 10 Realmente Melhora

Vamos começar pelo que funciona de verdade.

O .NET 10 melhora partes do pipeline que rodam em toda requisição. Parsing de headers no Kestrel, roteamento de endpoints, serialização JSON com System.Text.Json, eficiência de alocações em hot paths, e comportamento do async/await. Pra uma API que processa milhões de requests por dia, economizar alguns microsegundos por request não é trivial. Multiplica isso por 80 milhões de chamadas e o impacto aparece no custo de CPU e infraestrutura.

Pense num endpoint simples:

app.MapGet("/ping", () => Results.Ok(new
{
    Status = "OK",
    Utc = DateTime.UtcNow
}));

Esse tipo de endpoint se beneficia bastante de melhorias no runtime. Por quê? Porque a maior parte do trabalho está dentro do próprio pipeline do ASP.NET Core. A resposta é pequena, a lógica é mínima. O framework é o protagonista. Se o framework fica mais eficiente, esse endpoint fica mais rápido de verdade.

Mas agora compara com um endpoint de produção:

app.MapGet("/orders/{id:int}", async (
    int id,
    AppDbContext db,
    CancellationToken cancellationToken) =>
{
    var order = await db.Orders
        .Include(o => o.Items)
        .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);

    return order is null ? Results.NotFound() : Results.Ok(order);
});

Se esse endpoint é lento porque a tabela Orders não tem um índice adequado, o .NET 10 não vai transformar ele num endpoint rápido. O runtime não conserta um query plan ruim nem encurta um round trip de rede pro banco. Aquele N+1 escondido na camada de serviço? Continua lá.

Na prática, o .NET 10 reduz o custo de CPU e alocação ao redor da query. O envelope fica mais barato. O conteúdo continua do mesmo tamanho.

O Que o .NET 10 NÃO Melhora (E Nunca Vai)

Aqui é onde a conversa fica desconfortável. E é onde a maioria das equipes prefere não olhar.

A maioria das APIs lentas que eu já vi não eram lentas por causa do framework. Eram lentas por decisões de design. Queries sem índice, payloads retornando 30 campos quando o frontend precisava de 3. Código que bloqueia thread com .Result dentro de um método async. Coisas que nenhum runtime novo vai consertar.

Já perdi tempo otimizando serialização JSON quando o problema mesmo era um SELECT sem WHERE adequado. Gastei tempo precioso analisando flame graphs do serializer enquanto o banco estava sentado ali, fazendo full table scan, esperando alguém perceber. Lição aprendida: antes de culpar o framework, meça onde o tempo realmente vai.

Um exemplo que vejo direto, e você provavelmente já viu também:

var users = await db.Users.ToListAsync(cancellationToken);
var result = users
    .Where(x => x.IsActive)
    .Select(x => new UserDto { Id = x.Id, Name = x.Name })
    .ToList();

Esse código carrega todos os usuários pra memória e depois filtra no C#. O banco fez um trabalho enorme pra nada. A versão correta empurra o filtro pro banco:

var result = await db.Users
    .Where(x => x.IsActive)
    .Select(x => new UserDto
    {
        Id = x.Id,
        Name = x.Name
    })
    .ToListAsync(cancellationToken);

E isso ajuda na memória também. Menos objetos criados no processo, menos trabalho pro GC. E o GC, por mais eficiente que seja no .NET 10, ainda precisa limpar a bagunça que o seu código cria.

Ou seja: o runtime ficou mais rápido. Mas se o seu gargalo é um SELECT sem índice ou um N+1 que ninguém percebeu, o runtime não vai te salvar. Essa parte continua sendo sua.

A Verdade Sobre Cache em APIs de Alto Tráfego

Agora vem a parte que eu sei que vai gerar discussão. E estou confortável com isso.

Toda vez que alguém fala de performance em API, a primeira recomendação é: "coloca um cache." A lógica parece imbatível: a query mais rápida é aquela que você nunca executa. Parece genial.

Mas vamos ser honestos. Cache é uma das ferramentas mais mal utilizadas em sistemas de produção. E eu falo isso por experiência própria.

Sim, eu também, já coloquei cache em tudo achando que era a solução definitiva. Aprendi na marra que cache sem estratégia é só complexidade grátis.

O que ninguém menciona quando recomenda "coloca cache" é tudo que vem junto.

Começa com invalidação. Quando o dado muda, como você garante que o cache reflete isso? Se a resposta for "TTL de 5 minutos", você aceitou 5 minutos de dado potencialmente errado servindo pros seus usuários. Dependendo do domínio, isso é um bug, não uma feature.

Depois vem a consistência. Se a API roda em múltiplas instâncias, memory cache local vira um problema. Cada instância tem sua própria versão da "verdade". A instância A mostra o preço antigo, a B mostra o novo. O usuário acha que é bug. Porque é.

E tem o efeito mais traiçoeiro: cache esconde problemas. O endpoint é lento? Coloca cache. Pronto, ninguém mais investiga por que era lento. Até que o cache expire sob carga pesada e todo mundo descubra ao mesmo tempo.

Ah, e se for distributed cache, você ganhou mais um componente pra monitorar e mais um ponto de falha que pode dar timeout às 3 da manhã.

E aqui entra o que eu considero o ponto mais importante: uma leitura de banco bem indexada a 2-3ms muitas vezes é rápida o suficiente. Não precisa de cache. Precisa de um índice bom e uma query que pede só o que realmente precisa.

Pense numa busca de produto por ID com projeção direta:

var product = await db.Products
    .Where(p => p.Id == id)
    .Select(p => new ProductResponse
    {
        Id = p.Id,
        Name = p.Name,
        Price = p.Price
    })
    .FirstOrDefaultAsync(cancellationToken);

Com um índice no ID — que já existe como primary key — e uma projeção que retorna só o necessário, essa query volta em poucos milissegundos. Mesmo sob tráfego pesado. O banco de dados foi projetado exatamente pra isso.

Quando cache realmente faz sentido? Quando o cálculo é caro de verdade. Agregações complexas que varrem milhões de linhas, resultados de APIs externas com rate limit, dados que realmente não mudam tipo tabelas de configuração ou feature flags. Aí sim, cache faz sentido.

Mas cache como reflexo automático pra qualquer endpoint que parece lento? Isso é trocar um problema que você sabe resolver (query ruim, índice faltando) por um problema que é difícil de resolver direito: invalidação de cache em sistemas distribuídos.

A query mais perigosa não é a lenta. É a que você nunca roda e serve dado velho achando que está performando.

Dito isso, não estou dizendo "nunca use cache." Estou dizendo: meça primeiro. Se a leitura do banco já é rápida o suficiente pro seu caso de uso, não adicione complexidade sem necessidade. Cache deveria ser uma decisão consciente, não o primeiro reflexo quando alguém reclama de latência.

Async, Middleware e o Código Que Você Controla

Fora do banco e do cache, tem muito código no caminho de cada request que muita gente ignora.

Middleware, por exemplo. Cada middleware adicionado ao pipeline é código que roda em toda requisição. Uma string interpolation aqui, um Console.WriteLine ali. Parece pouco. Em APIs de baixo volume, é irrelevante mesmo. Em APIs processando 20 mil requests por segundo, cada alocação desnecessária no hot path vira custo real.

O .NET 10 melhora o overhead interno do framework. Mas não remove o lixo que o seu middleware customizado produz. Se o seu middleware aloca pesado ou faz logging síncrono, você está anulando os ganhos da plataforma.

Outro ponto que pega muita gente: async mal feito. Quantas vezes você já viu algo assim em code review?

app.MapGet("/reports/{id:int}", (int id, ReportService svc) =>
{
    var report = svc.GetReportAsync(id).Result;
    return Results.Ok(report);
});

Isso chama um método async mas bloqueia no .Result. Sob carga, isso causa thread starvation e mata o throughput. O endpoint "parece moderno" porque o service retorna Task<T>, mas o request path é efetivamente síncrono. A correção é simples:

app.MapGet("/reports/{id:int}", async (
    int id,
    ReportService svc,
    CancellationToken cancellationToken) =>
{
    var report = await svc.GetReportAsync(id, cancellationToken);
    return report is null ? Results.NotFound() : Results.Ok(report);
});

E o oposto do bloqueio também causa problemas — awaits sequenciais pra operações independentes:

var customer = await customerService.GetCustomerAsync(id, ct);
var orders = await orderService.GetOrdersAsync(id, ct);
var balance = await billingService.GetBalanceAsync(id, ct);

Se essas três chamadas são independentes, por que esperar uma terminar pra começar a próxima? Cada await sequencial adiciona latência sem necessidade:

var customerTask = customerService.GetCustomerAsync(id, ct);
var ordersTask = orderService.GetOrdersAsync(id, ct);
var balanceTask = billingService.GetBalanceAsync(id, ct);

await Task.WhenAll(customerTask, ordersTask, balanceTask);

Vale ressaltar: paralelismo aumenta pressão nos serviços downstream. Não é pra usar em toda situação. Mas pra chamadas independentes que vão pra serviços diferentes, a redução de latência pode ser significativa.

E sobre a discussão Minimal APIs vs Controllers: na minha experiência, o que importa mais é o que o endpoint faz, não como ele está declarado. Um endpoint enxuto performa bem nos dois estilos. Um endpoint que mistura validação, mapeamento, lógica de negócio, autorização e notificação numa única função performa mal independente do estilo. Bom design de endpoint ganha primeiro. Preferência de framework vem depois.

Meça Antes de Celebrar

Uma das armadilhas mais comuns quando sai uma versão nova do .NET: a equipe atualiza, roda uns testes locais, vê que a resposta parece mais rápida, e declara vitória. Na reunião seguinte: "atualizamos pro .NET 10, a API ficou mais rápida."

Mas "parece mais rápido" não é métrica. API de alto tráfego não pode depender de feeling.

Meça o p50, p95 e p99 de latência. Averages escondem dor. Um serviço que parece bem na média pode ter um p99 horrível, e em alto tráfego isso significa milhares de requests por minuto com experiência ruim.

Rode load tests com tráfego realista. Ferramentas como k6, Bombardier ou JMeter simulam concorrência mais próxima do que a API enfrenta de verdade. Um script simples de k6 já dá mais insight que 50 refreshes no browser:

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
    vus: 100,
    duration: '60s',
};

export default function () {
    http.get('https://localhost:5001/products/1');
    sleep(0.1);
}

E olhe pra dentro da aplicação. Tracing, métricas, logging estruturado. Você sabe quantos milissegundos o banco demora vs quantos o framework gasta em cada request? Se a resposta for não, qualquer conclusão sobre performance é chute.

A diferença entre uma API mais rápida e uma história de API mais rápida é disciplina de medição.

Conclusão — .NET 10 É Alavanca, Não Resgate

O .NET 10 traz melhorias que valem a pena. Pipeline de request mais eficiente, serialização JSON usando menos CPU, alocações menores em hot paths. Pra APIs que já estão razoavelmente bem desenhadas, esses ganhos multiplicados por milhões de requests podem significar economia de verdade em infraestrutura.

Mas, e aqui eu volto pro ponto que abriu esse artigo: o runtime não conserta código ruim. SQL lento continua lento. .Result num método async continua matando threads. E cache usado sem critério continua escondendo problemas em vez de resolvê-los.

As equipes que mais se beneficiam do .NET 10 não são as que esperam mágica. São as que conseguem dizer: "o runtime melhorou aqui, mas o nosso gargalo é ali." Não é uma frase empolgante. Mas é a que melhora sistemas em produção.

Atualizar o runtime é o passo mais fácil. O difícil é olhar pro próprio código e aceitar que a lentidão talvez seja responsabilidade sua.

Já passou por algo parecido? Atualizou o runtime e descobriu que o problema era outro? Deixe nos comentários qual foi a maior surpresa que você teve ao atualizar a versão do .NET.

[]s e até a próxima

Performance no ASP.NET Core

System.Text.Json

Benchmarking e Observabilidade

Entity Framework Core

Confira mais:

Fique por dentro das novidades

Assine nossa newsletter e receba as últimas atualizações e artigos diretamente em seu email.

Assinar gratuitamente