A API começa a ficar lenta. O consumo de CPU aumenta, o autoscaler identifica a pressão e adiciona novas instâncias. Durante alguns minutos, a latência parece melhorar. Logo depois, o banco começa a rejeitar conexões, o serviço de pagamento passa a responder lentamente e a taxa de timeout cresce. Os clientes fazem retry, mais requisições entram no sistema e a degradação se espalha.
O mecanismo de escala funcionou exatamente como foi configurado. O problema é que ele escalou apenas uma parte do caminho.
Adicionar instâncias aumenta a capacidade de processamento da aplicação quando o gargalo está dentro dela e o trabalho pode ser distribuído. Mas isso não aumenta automaticamente a capacidade do banco, do pool de conexões, da fila, do cache, do serviço de pagamento ou de qualquer outra dependência compartilhada.
Em alguns casos, o autoscaling não resolve o gargalo. Ele apenas cria mais produtores pressionando o mesmo recurso limitado.
É por isso que capacidade deveria ser analisada como uma propriedade do fluxo completo. Um sistema não suporta mais requisições apenas porque a primeira camada ganhou mais réplicas. Ele suporta aquilo que o caminho inteiro consegue processar de maneira sustentável.
Capacidade é uma cadeia, não uma soma
Considere um fluxo simplificado de checkout:
Cliente
↓
API de checkout — até 800 requisições por segundo
↓
Banco de dados — até 300 operações simultâneas
↓
Serviço antifraude — até 100 análises simultâneas
↓
Provider de pagamento — até 50 autorizações simultâneas
O fato de a API conseguir receber 800 requisições por segundo não significa que o checkout inteiro consiga concluir 800 operações por segundo. Se cada requisição precisa chamar o provider de pagamento e ele aceita apenas 50 operações simultâneas, essa dependência participa diretamente da capacidade efetiva do fluxo.
A análise também precisa considerar a quantidade de trabalho gerada por requisição. Um checkout pode realizar três consultas ao banco, duas escritas, uma análise antifraude e uma autorização de pagamento. Nesse caso, mil requisições na entrada não representam mil operações para cada dependência. Podem representar três mil leituras, duas mil escritas e mil chamadas externas.
A capacidade não deve ser comparada apenas em requisições por segundo. Antes, é preciso normalizar a unidade de trabalho produzida em cada etapa.
1 checkout
├── 3 leituras no banco
├── 2 escritas
├── 1 análise antifraude
└── 1 autorização de pagamento
Quando o volume de entrada cresce, todas essas operações crescem junto. Escalar a API pode aumentar a velocidade com que o trabalho chega às dependências, mas não a velocidade com que elas conseguem concluí-lo.
O paradoxo do autoscaling
Imagine uma aplicação com cinco instâncias. Cada instância aceita até vinte requisições simultâneas para uma integração externa. Em teoria, a aplicação pode abrir cem chamadas concorrentes.
O provider, porém, suporta apenas cinquenta chamadas simultâneas.
5 instâncias × 20 chamadas = 100 chamadas possíveis
Capacidade do provider = 50 chamadas simultâneas
A aplicação já consegue produzir duas vezes mais trabalho do que a dependência suporta. Agora imagine que o autoscaler aumente o número de instâncias de cinco para quinze:
15 instâncias × 20 chamadas = 300 chamadas possíveis
Capacidade do provider = 50 chamadas simultâneas
A camada da aplicação ganhou capacidade local, mas o limite do provider permaneceu igual. O sistema agora consegue pressionar a dependência seis vezes acima de sua capacidade.
O efeito pode aparecer de diversas formas:
- mais chamadas esperando conexão;
- aumento de latência;
- timeouts;
- filas internas crescendo;
- threads ou tarefas aguardando I/O;
- retries aumentando o tráfego;
- circuit breakers abrindo;
- consumo maior de memória;
- perda de capacidade para operações saudáveis.
Nesse cenário, adicionar instâncias não aumentou a capacidade do fluxo. Aumentou apenas o número de chamadas em disputa pelo mesmo recurso.
CPU baixa não significa capacidade disponível
Outro problema é assumir que CPU representa toda a capacidade da aplicação.
Um serviço pode apresentar CPU baixa enquanto está completamente saturado. Isso acontece quando a maior parte do tempo é gasta esperando:
- conexões do banco;
- respostas HTTP;
- espaço em uma fila;
- locks;
- acesso a arquivos;
- tokens de um rate limiter;
- capacidade de outro serviço;
- conclusão de operações externas.
Imagine uma API com CPU em 35%, mas cujo pool de conexões está 100% ocupado. Criar novas instâncias pode reduzir a disputa local por CPU, mas também pode criar novos pools, abrir mais conexões e aumentar ainda mais a pressão sobre o banco.
O mesmo vale para serviços externos. Uma aplicação esperando respostas lentas pode consumir pouca CPU. Para o autoscaler, talvez pareça saudável. Para o usuário, a latência está aumentando e o sistema está perto do colapso.
CPU e memória continuam sendo sinais importantes, mas nem sempre representam a unidade que limita o fluxo. Dependendo do sistema, métricas mais úteis podem ser:
- operações em andamento;
- utilização do pool de conexões;
- latência da dependência;
- mensagens pendentes;
- idade da mensagem mais antiga;
- taxa de entrada e de processamento;
- chamadas rejeitadas;
- tempo em fila;
- percentual de timeouts;
- capacidade contratual restante.
Escalar com base na métrica errada é otimizar a parte visível enquanto o verdadeiro gargalo permanece escondido.
O autoscaler precisa observar uma métrica que responda à escala
Uma boa métrica de autoscaling deve melhorar quando novas instâncias são adicionadas.
CPU por instância normalmente se comporta assim: quando o trabalho é distribuído entre mais réplicas, a média tende a cair. O mesmo pode acontecer com requisições por instância ou mensagens processadas por worker.
Mas nem toda métrica responde à escala da aplicação. Se o banco está com 100% das conexões ocupadas, adicionar pods não fará essa utilização cair. Pode ocorrer exatamente o contrário.
O mesmo raciocínio vale para um provider com limite global de chamadas. O número de requisições simultâneas na dependência não diminui apenas porque existem mais instâncias na origem.
Antes de usar uma métrica para escalar, vale fazer uma pergunta simples:
Quando eu adicionar uma réplica, essa métrica tende a melhorar ou estou apenas aumentando a pressão sobre outro recurso?
Se a resposta for incerta, talvez o problema não deva ser resolvido apenas com escala horizontal.
O orçamento de conexões também precisa escalar
Um erro comum em aplicações com banco de dados é configurar o pool de conexões por instância sem considerar o número total de réplicas.
Imagine um banco que suporta, com segurança, 300 conexões para determinada aplicação. O serviço começa com cinco instâncias, cada uma configurada com um pool máximo de 40 conexões:
5 instâncias × 40 conexões = 200 conexões possíveis
Ainda existe alguma margem. Quando o autoscaler aumenta para quinze instâncias:
15 instâncias × 40 conexões = 600 conexões possíveis
O banco não passou a suportar 600 conexões porque a aplicação ganhou mais réplicas. O orçamento global permaneceu em 300.
Isso significa que configurações locais podem produzir um comportamento global perigoso. O pool por instância, o número máximo de operações simultâneas e a quantidade de workers precisam considerar a escala máxima do serviço.
Uma forma simplificada de pensar é:
Orçamento global disponível
÷
Número máximo esperado de instâncias
=
Orçamento aproximado por instância
Esse cálculo não precisa ser aplicado de forma rígida, porque a distribuição de carga e o uso das conexões variam. Mas ele deixa claro que cada réplica consome parte de um recurso compartilhado.
Sem um orçamento global, o autoscaling pode transformar uma configuração local aparentemente segura em uma sobrecarga coletiva.
Proteja a dependência perto de onde ela é chamada
Quando existe uma dependência com capacidade limitada, uma boa prática é controlar quantas chamadas podem chegar até ela simultaneamente. Esse controle é conhecido como limite de concorrência.
A ideia é simples: mesmo que a aplicação receba mais tráfego ou ganhe mais instâncias, o número de operações em andamento contra a dependência permanece dentro de um limite conhecido.
Em .NET, o conceito pode ser representado com um ConcurrencyLimiter:
private readonly ConcurrencyLimiter _limiter = new(
new ConcurrencyLimiterOptions
{
PermitLimit = 20,
QueueLimit = 10,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst
});
public async Task<PaymentResult> AuthorizeAsync(
PaymentRequest request,
CancellationToken cancellationToken)
{
using var lease = await _limiter.AcquireAsync(
permitCount: 1,
cancellationToken);
if (!lease.IsAcquired)
{
throw new PaymentCapacityExceededException();
}
return await _provider.AuthorizeAsync(
request,
cancellationToken);
}
A API específica muda em Java, Go, Python ou JavaScript, mas o desenho é o mesmo:
receber operação
tentar adquirir capacidade
se houver capacidade:
chamar dependência
senão:
rejeitar ou aguardar dentro de um limite
liberar capacidade ao terminar
O exemplo possui uma limitação importante: o limiter está dentro do processo. Se existem dez instâncias e cada uma permite vinte chamadas, o limite global pode chegar a duzentas.
Por isso, o valor local precisa considerar o número de réplicas, ou o controle deve ser aplicado em uma camada compartilhada, como gateway, proxy, service mesh, coordenador distribuído ou no próprio provider.
O objetivo não é colocar um semáforo em todo lugar. É impedir que o número de produtores cresça sem respeitar a capacidade do recurso consumido.
Fila limitada é diferente de espera infinita
Quando todas as permissões estão ocupadas, a aplicação precisa decidir o que fazer com o novo trabalho.
Uma opção é enfileirar temporariamente. Isso pode suavizar rajadas pequenas. Porém, a fila precisa ser limitada. Uma fila ilimitada não cria capacidade; ela apenas transforma saturação em memória ocupada e latência crescente.
Se a dependência processa cinquenta operações por segundo e chegam cem, a fila cresce em cinquenta operações a cada segundo. Depois de um minuto, existem aproximadamente três mil operações aguardando. Mesmo que o tráfego volte ao normal, o sistema ainda precisa processar o atraso acumulado.
Nesse momento, algumas requisições talvez já tenham perdido valor. O usuário pode ter fechado a tela, o timeout pode ter expirado e o cliente pode ter iniciado outra tentativa.
Uma fila saudável precisa ter pelo menos:
- tamanho máximo;
- tempo máximo de espera;
- política de rejeição;
- cancelamento;
- observabilidade;
- definição de prioridade, quando necessária.
Quando não há espaço, rejeitar cedo costuma ser melhor do que aceitar trabalho que provavelmente terminará depois do prazo esperado.
Rejeitar pode preservar disponibilidade
Existe uma tendência de considerar qualquer rejeição como falha. Em sistemas saturados, porém, tentar aceitar tudo pode fazer todas as operações falharem lentamente.
Considere um checkout que executa quatro partes:
1. calcular recomendações
2. consultar benefícios promocionais
3. autorizar pagamento
4. confirmar pedido
Durante uma degradação, as duas primeiras talvez possam ser simplificadas ou desativadas. A autorização e a confirmação precisam permanecer disponíveis.
Em vez de consumir toda a capacidade tentando manter o fluxo completo, o sistema pode praticar degradação controlada:
Recomendações: desativadas
Benefícios secundários: cache ou resposta simplificada
Pagamento: prioridade alta
Confirmação do pedido: prioridade alta
Quando nem mesmo o fluxo essencial cabe na capacidade restante, load shedding permite rejeitar parte da carga antes que ela atravesse o sistema inteiro.
Isso não significa abandonar usuários aleatoriamente. A política pode considerar prioridade, cliente, operação, custo ou prazo. O ponto é preservar uma parte saudável do serviço em vez de permitir que a saturação destrua tudo ao mesmo tempo.
Retry pode multiplicar a carga
Quando uma dependência começa a falhar ou ficar lenta, retries podem parecer a resposta natural. Se a falha for transitória, uma nova tentativa pode funcionar.
O problema é que retry também cria trabalho.
Imagine mil requisições, cada uma com até três tentativas:
1.000 operações de entrada
×
até 3 tentativas
=
até 3.000 chamadas downstream
A dependência que já estava degradada pode receber até três vezes mais tráfego. Se cada camada do sistema também fizer seus próprios retries, a amplificação pode ser ainda maior.
Uma estratégia saudável precisa considerar:
- quais erros realmente merecem retry;
- quantas tentativas são permitidas;
- backoff;
- jitter;
- deadline total;
- idempotência;
- orçamento global de tentativas;
- estado do circuit breaker;
- capacidade disponível.
Retry sem orçamento pode transformar uma falha parcial em incidente em cascata. Durante sobrecarga, talvez a melhor decisão não seja tentar novamente, mas reduzir carga, degradar funcionalidade ou rejeitar cedo.
Um exemplo completo: o checkout que escalou e ficou pior
Imagine uma API de checkout com a seguinte configuração inicial:
API:
5 instâncias
40 requisições simultâneas por instância
Banco:
300 conexões disponíveis
Antifraude:
100 análises simultâneas
Provider de pagamento:
50 autorizações simultâneas
Em uma campanha, o tráfego cresce. A CPU da API ultrapassa o limite configurado e o autoscaler aumenta de cinco para quinze instâncias.
A capacidade local da API passa de 200 para 600 requisições simultâneas. Entretanto, banco, antifraude e pagamento continuam com os mesmos limites.
Cada nova instância também cria um pool de conexões e começa a enviar chamadas. O banco se aproxima do limite. O antifraude aumenta a latência. O provider de pagamento começa a rejeitar solicitações. A aplicação faz retry. As requisições permanecem mais tempo em andamento e o consumo de memória aumenta.
O dashboard mostra muitas instâncias disponíveis, mas o fluxo entrega menos pedidos concluídos.
Uma solução melhor não seria simplesmente aumentar novamente o máximo de réplicas. O time precisaria tratar a capacidade como um sistema:
API:
escala por demanda e latência
Banco:
orçamento global de conexões
Antifraude:
limite de concorrência e modo degradado
Pagamento:
limite global de chamadas e fila curta
Retries:
poucas tentativas, backoff e jitter
Checkout:
priorização das etapas essenciais
Excesso:
rejeição previsível antes da saturação
O autoscaling continua tendo valor. Ele apenas deixa de ser a única defesa.
Teste o fluxo, não apenas o componente
Um serviço pode apresentar excelente resultado em teste isolado e falhar quando conectado ao restante do sistema.
A API pode processar mil requisições por segundo usando mocks. O banco pode executar determinada query rapidamente em teste. O provider pode cumprir sua latência média. Nada disso prova que o fluxo completo suporta mil checkouts por segundo.
Um teste de capacidade relevante precisa incluir:
- proporção real de chamadas;
- concorrência;
- tamanhos de payload;
- comportamento de caches;
- pools de conexão;
- limites externos;
- retries;
- timeouts;
- filas;
- tempo de aquecimento das instâncias;
- degradação de dependências.
Também é importante ultrapassar o limite planejado. O objetivo não é apenas descobrir quanto o sistema suporta, mas como ele falha quando essa capacidade termina.
Um sistema saudável deveria degradar de forma previsível. Um sistema frágil apresenta filas crescentes, timeouts em cascata, retries descontrolados e recuperação lenta mesmo depois que o tráfego diminui.
Métricas que mostram o gargalo real
CPU e memória continuam importantes, mas precisam ser complementadas com métricas do fluxo.
Para uma API com banco e provider externo, eu observaria pelo menos:
| Camada | Métrica relevante |
|---|---|
| Entrada | requisições por segundo e concorrência |
| Aplicação | latência, CPU, memória e operações em andamento |
| Pool | conexões utilizadas, espera e timeouts |
| Banco | queries, locks, latência e saturação |
| Fila | tamanho, taxa de entrada, taxa de saída e idade da mensagem mais antiga |
| Dependência | concorrência, latência, erros, rejeições e throttling |
| Resiliência | retries, circuit breaker, fallback e load shedding |
| Negócio | operações concluídas por segundo |
A última métrica é especialmente importante. O objetivo não é ter mais pods nem processar mais chamadas internas. O objetivo é concluir mais operações úteis.
Se o número de instâncias aumenta enquanto pedidos concluídos diminuem, o autoscaling está reagindo ao sintoma, não resolvendo a capacidade do sistema.
Quando escalar horizontalmente funciona bem
O argumento não é que autoscaling seja ruim. Ele funciona muito bem quando algumas condições são verdadeiras:
- o gargalo está na camada escalada;
- o trabalho pode ser distribuído;
- as dependências possuem capacidade proporcional;
- o particionamento não cria contenção;
- o tempo de inicialização é compatível com a demanda;
- a métrica usada representa trabalho;
- o sistema limita pressão downstream.
Uma API que executa processamento local de CPU pode ganhar capacidade quase proporcional ao número de instâncias. Workers consumindo partições independentes também podem escalar bem. Serviços stateless com dependências igualmente escaláveis são bons candidatos.
O problema é aplicar o mesmo raciocínio a qualquer fluxo, inclusive quando todas as réplicas dependem do mesmo banco, lock, arquivo, provider ou limite contratual.
Escala horizontal não elimina recursos compartilhados. Em muitos casos, ela faz com que esses recursos sejam alcançados mais rapidamente.
Um roteiro prático para capacidade de ponta a ponta
Antes de aumentar réplicas, mapeie o fluxo e identifique as dependências atravessadas por cada operação. Para cada uma, estime a capacidade, a unidade de trabalho e o comportamento quando o limite é atingido.
Depois, compare capacidade de produção e de consumo entre as etapas. Se a aplicação consegue gerar 500 chamadas simultâneas para uma dependência que suporta 50, existe um desacoplamento que precisa ser controlado.
Defina limites de concorrência, pools e filas considerando o orçamento global, não apenas cada instância. Garanta que retries tenham orçamento e que operações não essenciais possam ser degradadas. Escolha métricas de autoscaling que respondam à adição de réplicas e acompanhe a quantidade de operações de negócio concluídas.
Por fim, teste acima da capacidade. Descubra se o sistema rejeita cedo, acumula fila, entra em timeout ou amplia tráfego com retries. A capacidade máxima importa, mas o comportamento depois dela importa ainda mais.
Fechando a ideia
Adicionar instâncias aumenta recursos em uma camada. Isso não significa que o sistema inteiro ganhou capacidade.
Se todas as novas réplicas disputam o mesmo banco, pool, fila ou provider, o autoscaling pode apenas acelerar a chegada ao gargalo. Em situações piores, ele amplia a pressão, aumenta a concorrência, dispara retries e transforma uma degradação localizada em incidente distribuído.
Capacidade precisa ser desenhada de ponta a ponta. Isso inclui budgets de conexão, limites de concorrência, filas limitadas, backpressure, load shedding, retries controlados, métricas adequadas e testes que atravessem o fluxo real.
A pergunta não deveria ser apenas “quantas instâncias esta aplicação consegue criar?”. A pergunta mais importante é:
Quanto trabalho útil o caminho completo consegue concluir sem ultrapassar a capacidade de sua parte mais restritiva?
É essa resposta que define a capacidade do sistema. O restante é apenas capacidade local.
