Entenda por que sistemas web degradam sob carga, como diferenciar bloqueio do executor de saturação de serviços downstream e quais técnicas aumentam a resiliência.

Confira!

Uma aplicação web pode parecer “travada” por razões arquiteturalmente muito diferentes. Em um caso, uma tarefa bloqueia o mecanismo que deveria coordenar outras requisições; em outro, a aplicação continua responsiva, mas gera carga suficiente para saturar um serviço downstream. CPU elevada, timeouts, filas crescentes, erros 502/503/504 e health checks falhando são sintomas possíveis — não diagnósticos.

O ponto central é tratar capacidade como propriedade do sistema inteiro. Cada requisição consome uma combinação de CPU, memória, conexões, filas, threads ou outros executores, largura de banda e capacidade de serviços dependentes. Quando o trabalho criado supera a capacidade de processamento, ou quando uma operação impede outras de progredir, a latência cresce, o número de requisições em voo aumenta e mecanismos de proteção começam a ser pressionados. A engenharia de performance precisa identificar qual recurso deixou de acompanhar a demanda e por quê, antes de concluir que “falta servidor”.

Por que uma aplicação trava mesmo sem estar fora do ar

“Travamento” é uma descrição da experiência do usuário. Para a arquitetura, é preciso decompor o sintoma. Um serviço pode continuar aceitando conexões, mas responder lentamente; pode ter CPU disponível e ainda assim estar sem workers; pode ter workers livres e aguardar conexões de banco; pode estar saudável localmente e bloquear enquanto espera um downstream saturado.

Essa distinção é essencial porque intervenções aparentemente óbvias podem atacar o recurso errado. Aumentar CPU não corrige uma fila causada por uma dependência lenta. Aumentar workers pode piorar a pressão sobre um banco de dados já saturado. Fazer retry agressivo sobre um serviço em overload pode transformar uma degradação parcial em falha em cascata.

A observabilidade de sistemas, aplicações e serviços digitais precisa, portanto, correlacionar sintomas externos com sinais internos de utilização, espera e saturação.

O modelo mental: entrada, fila, capacidade e saída

Todo caminho de processamento pode ser representado, de forma simplificada, como uma sequência de chegada, espera, execução, dependências e resposta.

Modelo simplificado de capacidade e saturação em um sistema web

Requisições

Admissão

Fila

Executor ou workers

CPU e memória

Serviços downstream

Banco, APIs ou storage

Resposta

Modelo simplificado de capacidade e saturação em um sistema web

Se a taxa de chegada permanecer abaixo da capacidade efetiva, o sistema tende a encontrar um regime estável. Quando a taxa de trabalho útil supera a capacidade por tempo suficiente, a fila cresce. O aumento da fila adiciona espera; a espera aumenta a latência; a latência mantém mais requisições simultaneamente em voo; essas requisições consomem mais memória, conexões e estruturas internas. O resultado pode ser uma realimentação positiva.

A notação de teoria de filas ajuda a organizar o raciocínio:

  • λ representa a taxa média de chegada;
  • μ representa a capacidade média de serviço de um recurso;
  • se λ permanecer maior que μ, aquele recurso não possui um estado estacionário sustentável sem rejeição, redução de carga ou aumento de capacidade.

Em um sistema estável, a Lei de Little relaciona o número médio de itens no sistema (L), a taxa média de conclusão (λ) e o tempo médio no sistema (W): L = λW. Na prática, isso ajuda a entender por que uma elevação de latência pode multiplicar rapidamente o número de requisições em voo mesmo quando o tráfego de entrada não mudou.

Concorrência não é paralelismo

Concorrência significa estruturar o sistema para que múltiplas tarefas possam progredir durante intervalos de tempo sobrepostos. Paralelismo significa executar trabalho simultaneamente em múltiplos recursos de processamento. Um sistema pode ser concorrente sem ser paralelo: um único executor pode alternar entre várias operações sempre que elas cedem o controle enquanto aguardam I/O.

Essa diferença é importante em aplicações orientadas por eventos. O modelo funciona muito bem quando operações potencialmente lentas suspendem sua execução em pontos apropriados e permitem que outras tarefas avancem. Porém, uma operação síncrona longa executada no caminho principal pode monopolizar o executor.

O problema não é que “assíncrono é lento”. O problema é violar o modelo de concorrência escolhido pela aplicação.

Primeiro tipo de falha: o executor principal fica bloqueado

Considere um serviço que coordena milhares de requisições sobre um pequeno número de executores. A maior parte do trabalho é I/O e pode ser intercalada eficientemente. Em determinado endpoint, porém, entra uma operação síncrona longa: uma varredura de arquivos, uma transformação pesada, uma expressão regular sobre grande volume de dados, compressão, parsing custoso ou uma chamada de rede por cliente bloqueante.

Enquanto essa operação não devolve o controle, outras tarefas que dependem do mesmo executor deixam de progredir. A consequência pode incluir:

  • aumento abrupto de latência em endpoints que não têm relação funcional com a tarefa pesada;
  • starvation de requisições pequenas;
  • health checks que deixam de responder a tempo;
  • timeouts nos clientes;
  • crescimento de filas internas mesmo sem saturação do servidor inteiro;
  • baixa utilização de alguns recursos ao mesmo tempo em que o serviço parece indisponível.

A correção arquitetural típica é isolar o trabalho incompatível com o executor principal. Dependendo da natureza da tarefa, isso pode significar usar um pool separado de threads, processos, workers de background ou outro componente especializado. Trabalho CPU-bound e I/O bloqueante não precisam necessariamente da mesma estratégia.

O princípio é mais geral: não permita que uma operação que não coopera com o mecanismo de concorrência monopolize o caminho responsável por coordenar as demais.

Segundo tipo de falha: o serviço downstream fica saturado

O segundo cenário é diferente. O serviço que recebe a interação do usuário pode estar saudável e continuar despachando trabalho corretamente. O gargalo aparece em uma dependência: outro serviço web, um banco de dados, um mecanismo de busca, uma API interna, um storage ou qualquer componente com capacidade finita.

Pools existem justamente porque recursos são limitados. Um serviço pode possuir 20, 50 ou 200 workers; um pool de banco pode aceitar um número máximo de conexões; uma API pode processar determinada quantidade de operações concorrentes antes que a latência cresça significativamente.

Quando todos os workers estão ocupados, novas requisições esperam. Se a espera cresce, os clientes podem atingir seus timeouts. Se esses clientes repetem a chamada, a carga aumenta justamente quando o downstream possui menos capacidade disponível.

Essa é uma diferença importante em relação ao primeiro caso: não existe necessariamente um executor bloqueado no serviço chamador. O chamador pode estar fazendo exatamente o que foi programado para fazer — só está produzindo trabalho demais para a capacidade efetiva da dependência.

Workers, filas e o custo de esperar

Filas absorvem rajadas curtas, mas não criam capacidade. Uma fila sem limite pode apenas adiar a falha e convertê-la em consumo de memória e latência extrema.

Imagine um pool de workers em que cada operação leva 100 ms. Se a demanda é relativamente constante e a fila cresce para dez vezes o número de workers, uma requisição pode passar muito mais tempo esperando do que sendo processada. Em sistemas interativos, quando finalmente chegar ao worker, o cliente talvez já tenha abandonado a requisição.

Por isso, filas devem ser tratadas como um recurso arquitetural com política explícita:

DecisãoPergunta de engenharia
Tamanho máximoQuantos itens podem esperar sem degradar o objetivo de latência?
AdmissãoQuando uma nova requisição deve ser recusada?
PrioridadeTodo trabalho possui a mesma criticidade?
DeadlineAinda vale a pena processar este item quando chegar sua vez?
OverflowA carga excedente será rejeitada, degradada ou redirecionada?

Fail-fast, load shedding e graceful degradation não são necessariamente sinais de um sistema fraco. Em overload, podem ser mecanismos que preservam a capacidade de executar trabalho útil.

Fan-out: uma ação do usuário pode produzir dezenas de operações

A quantidade de interações de usuário é uma métrica insuficiente para estimar carga interna. Uma única ação pode disparar várias chamadas do frontend; cada chamada pode consultar múltiplos serviços; esses serviços podem, por sua vez, consultar bancos, caches e outras APIs.

Amplificação de carga por fan-out em uma única interação

1 ação do usuário

4 chamadas da interface

Serviço A

Serviço B

Serviço C

Consultas downstream

Dezenas de operações reais

Amplificação de carga por fan-out em uma única interação

O fenômeno pode ser expresso como uma amplificação 1 → N → M. Essa multiplicação é particularmente perigosa quando o trabalho downstream é caro ou redundante.

O problema N+1 é uma forma conhecida desse padrão, mas o risco é mais amplo. Componentes diferentes podem repetir a mesma listagem, a mesma busca de relacionamento ou a mesma agregação sem saber que outra chamada concorrente já está fazendo exatamente o mesmo trabalho.

Ao diagnosticar performance, portanto, uma métrica extremamente útil é quantas operações downstream são produzidas por uma única requisição lógica do usuário.

Payload também é carga

O custo de uma requisição não termina quando o banco conclui a consulta. Um objeto grande precisa ser construído, serializado, armazenado temporariamente em buffers, transferido pela rede, desserializado e processado pelo consumidor.

Retornar dados que o cliente não usa amplia vários custos simultaneamente:

  • leitura e hidratação de objetos no servidor;
  • CPU de serialização;
  • memória temporária;
  • largura de banda;
  • tempo de transferência;
  • parsing e alocação no consumidor;
  • pressão sobre garbage collection;
  • ocupação do worker por mais tempo.

Por isso, contar apenas requisições por segundo pode mascarar diferenças enormes. Dez requisições que retornam poucos kilobytes podem custar menos que uma requisição que monta e transfere vários megabytes.

A técnica de response projection consiste em selecionar apenas os campos necessários para aquela operação. A melhor API não é a que consegue retornar o maior objeto possível, mas a que permite obter o conjunto mínimo de dados necessário para resolver cada intenção.

Trabalho redundante: cache também é controle de capacidade

Cache costuma ser apresentado como técnica para “deixar mais rápido”. Em sistemas sob carga, seu papel é mais importante: evitar que o mesmo trabalho seja criado repetidamente.

Suponha que uma listagem cara possa ser reutilizada durante 60 segundos. Sem coordenação, dez requisições simultâneas após a expiração podem perceber o cache vazio e todas iniciar a mesma recomputação. É o padrão conhecido como cache stampede ou thundering herd.

Uma estratégia de request coalescing, também chamada em alguns contextos de single-flight, permite que a primeira chamada faça a atualização enquanto as demais aguardam ou reutilizam o mesmo resultado. O ganho não vem apenas do TTL; vem de impedir recomputações concorrentes idênticas.

Isso muda o comportamento de:

N requisições simultâneas → N buscas caras

para:

N requisições simultâneas → 1 busca cara → N consumidores do resultado

Um TTL curto pode, portanto, produzir grande redução de carga quando a informação é muito mais consultada do que alterada.

Backpressure: mais concorrência nem sempre significa mais throughput

Quando um downstream suporta 20 operações concorrentes de forma eficiente, permitir que 200 chamadas o atinjam ao mesmo tempo não multiplica automaticamente o throughput por dez. Frequentemente ocorre o oposto: contenção, filas, mudanças de contexto, uso de memória e disputa por conexões elevam a latência e reduzem a eficiência do trabalho útil.

Backpressure é o conjunto de mecanismos pelos quais um sistema impede que produtores gerem trabalho indefinidamente acima da capacidade dos consumidores. Pode envolver:

  • limites de concorrência;
  • filas limitadas;
  • throttling;
  • rate limiting;
  • semáforos;
  • quotas;
  • deadlines;
  • rejeição antecipada;
  • load shedding;
  • degradação controlada da resposta.

O objetivo não é eliminar picos. É impedir que um pico seja transformado em colapso por realimentação positiva.

Retries podem transformar uma falha pequena em uma tempestade

Retry é correto para certas falhas transitórias, mas é perigoso quando aplicado mecanicamente a overload. Se um serviço está recusando chamadas porque não possui capacidade, reenviar imediatamente cada falha cria mais trabalho para um recurso já pressionado.

Em arquiteturas em camadas, retries independentes podem se multiplicar. Se várias camadas repetem a mesma operação, uma única interação pode gerar dezenas de tentativas na dependência final.

Boas políticas incluem:

  • distinguir erros transitórios de erros permanentes;
  • limitar tentativas por requisição;
  • manter um retry budget global ou por cliente;
  • aplicar exponential backoff;
  • adicionar jitter para evitar que vários clientes repitam ao mesmo tempo;
  • evitar retries redundantes em múltiplas camadas;
  • propagar deadlines e cancelamentos;
  • garantir idempotência quando a operação puder ser repetida.

O Guia Completo sobre HTTP fornece a base protocolar para compreender status, semântica de requisições e comportamento entre cliente e servidor; a política de resiliência, porém, depende da arquitetura e do custo real de cada operação.

Falhas em cascata: quando o gargalo se propaga

Uma falha em cascata acontece quando a degradação de uma parte do sistema aumenta a probabilidade de degradação de outras partes. É um mecanismo de realimentação.

Um downstream lento mantém requisições abertas por mais tempo. O serviço upstream passa a acumular requisições em voo. Isso consome workers, conexões e memória. O pool se aproxima do limite. O health check passa a competir pelos mesmos recursos. Instâncias parecem indisponíveis e são retiradas do balanceamento. A carga então se concentra nas instâncias restantes.

A sequência pode ser resumida assim:

dependência lenta → mais latência → mais requisições em voo → mais recursos ocupados → menor capacidade útil → mais latência

Cache frio, retries, balanceamento inadequado e filas excessivas podem acelerar esse ciclo.

É por isso que engenharia de sistemas aplicada a serviços digitais precisa considerar interfaces, dependências, modos degradados e capacidade como propriedades da arquitetura, não apenas de componentes isolados.

CPU alta é um sinal, não um diagnóstico

CPU é uma métrica importante, mas começar e terminar o diagnóstico nela produz conclusões frágeis. Um sistema pode estar saturado por conexões, locks, workers, banco, memória ou fila enquanto a CPU permanece moderada. Também pode apresentar CPU alta como consequência secundária de outro fenômeno, como garbage collection provocado pelo aumento de requisições em voo.

Uma investigação de saturação deve correlacionar sinais:

SinalO que pode revelar
Latência p50/p95/p99Distribuição e cauda do tempo de resposta
Queue depthTrabalho aguardando capacidade
Requisições em vooPressão concorrente real
Workers ativos/livresSaturação do executor
Espera por conexãoPool de banco/API/storage como gargalo
CPU e memóriaPressão computacional e efeitos secundários
Bytes por respostaCusto de payload e serialização
Chamadas downstream por requisiçãoFan-out e amplificação
Cache hit ratioQuanto trabalho está sendo evitado
Retry rateCarga adicional causada por recuperação
Rejeições/429/503Backpressure e overload explícito
Deadlines excedidosTrabalho realizado tarde demais

O ponto é encontrar onde a latência começa e qual recurso está no limite naquele ponto.

Método prático para diagnosticar saturação

Uma investigação pode seguir uma sequência disciplinada:

  1. Defina o sintoma observável: latência, timeout, erro, indisponibilidade parcial ou perda de throughput.
  2. Localize a primeira fronteira em que a latência cresce: cliente, serviço de entrada, executor, banco ou downstream.
  3. Identifique o recurso finito associado: CPU, memória, worker, conexão, fila, lock ou capacidade externa.
  4. Meça a amplificação: quantas chamadas e operações nascem de uma interação lógica.
  5. Meça o custo unitário: CPU, tempo, bytes, consultas e dependências por operação.
  6. Procure trabalho redundante: buscas idênticas, recomputações, N+1 e cache misses concorrentes.
  7. Verifique os mecanismos de realimentação: retries, filas longas, autoscaling tardio, health checks e cache frio.
  8. Reduza trabalho e limite concorrência antes de assumir que a única solução é mais hardware.
  9. Teste sob carga até além do ponto nominal para verificar como o sistema falha e como se recupera.

A qualidade do diagnóstico depende de medir o caminho completo. Métricas agregadas do host são úteis, mas não substituem tracing, métricas por dependência e instrumentação de pools e filas.

Caso conceitual: dois gargalos diferentes com o mesmo sintoma

Um caso real de operação ilustra por que essa distinção importa. Os nomes das tecnologias não são relevantes; o comportamento arquitetural é.

Cenário A — bloqueio do mecanismo de concorrência

Um serviço orientado a I/O possuía um executor principal responsável por coordenar múltiplas requisições. Algumas rotas executavam operações síncronas longas no mesmo caminho. Durante essas operações, o executor deixava de ceder controle e até endpoints simples acumulavam espera.

O sintoma externo era “a API travou”. O mecanismo real era starvation provocado por trabalho bloqueante no executor responsável pela concorrência.

A correção foi deslocar as operações incompatíveis para um executor separado, preservando o caminho principal para coordenação das requisições.

Cenário B — saturação de uma dependência

Depois da primeira correção, surgiu outro padrão de falha. Uma única abertura de interface disparava dezenas de consultas caras a um serviço downstream. Algumas operações recuperavam objetos completos embora o consumidor precisasse de apenas poucos campos. Uma resposta chegou à ordem de vários megabytes.

Além disso, chamadas paralelas repetiam a mesma busca integral apenas para descobrir relacionamentos entre registros. O resultado foi ocupação simultânea de workers, serialização de payloads grandes e trabalho redundante sobre a mesma origem.

O sintoma externo novamente parecia “o sistema travou”, mas agora o serviço chamador continuava funcional. O gargalo era esgotamento da capacidade finita de workers no downstream.

A correção combinou duas medidas:

  • projeção de resposta para recuperar somente os campos efetivamente usados, reduzindo respostas de megabytes para ordens de magnitude menores;
  • cache de curta duração com coordenação de atualização, fazendo com que chamadas concorrentes compartilhassem uma única busca em vez de repetir a rajada completa.

O que o caso demonstra

Os dois incidentes produziam sintomas semelhantes, mas exigiam correções diferentes. No primeiro, era necessário proteger o mecanismo que distribuía o trabalho. No segundo, era necessário reduzir e coordenar o trabalho enviado a um recurso downstream.

Essa distinção pode ser resumida em uma pergunta diagnóstica:

o sistema parou de distribuir trabalho ou distribuiu mais trabalho do que algum componente consegue executar?

Otimizar antes de escalar infraestrutura

Escalar hardware pode ser a decisão correta, mas somente depois de compreender o perfil de carga. Aumentar capacidade sobre uma arquitetura que cria trabalho redundante torna o desperdício maior e pode apenas deslocar o gargalo.

Uma ordem de análise útil é:

  1. eliminar operações desnecessárias;
  2. reduzir o custo por operação;
  3. reutilizar resultados quando a consistência permitir;
  4. coalescer trabalho idêntico concorrente;
  5. limitar fan-out e concorrência;
  6. proteger downstreams com backpressure;
  7. definir timeouts, retries e deadlines de forma consistente;
  8. medir novamente;
  9. então dimensionar recursos adicionais.

Esse raciocínio não é uma defesa de “otimização prematura”. É engenharia de capacidade: antes de comprar mais capacidade, saber qual trabalho realmente precisa existir.

Princípios de projeto para sistemas resilientes

PrincípioObjetivo
Evitar trabalho bloqueante no executor críticoPreservar progresso das tarefas concorrentes
Controlar fan-outEvitar amplificação de carga
Projetar respostas mínimasReduzir CPU, memória e transferência por operação
Cachear resultados reutilizáveisEvitar recomputação e chamadas downstream
Coalescer misses simultâneosEvitar cache stampede e thundering herd
Limitar filasConter memória e latência de espera
Limitar concorrênciaManter recursos no regime eficiente
Aplicar backpressureFazer a produção respeitar a capacidade de consumo
Usar deadlines e cancelamentoEvitar trabalho que já perdeu utilidade
Controlar retriesEvitar retry storms
Fazer load shedding quando necessárioPreservar trabalho prioritário sob overload
Testar além da carga nominalConhecer o modo real de falha e recuperação

Arquitetura de software trata performance como atributo de qualidade, e não como ajuste feito somente depois que o código está pronto. O desenho de pools, filas, caches, interfaces e limites determina como o sistema se comportará quando a demanda se aproximar da capacidade.

Considerações finais

Sistemas web raramente degradam simplesmente porque “faltou CPU”. Eles degradam quando a quantidade de trabalho criada, a forma como esse trabalho é distribuído e a capacidade dos recursos deixam de estar equilibradas.

Dois princípios são especialmente úteis. Primeiro, bloquear quem coordena o trabalho é diferente de saturar quem executa o trabalho. Segundo, fila, retry e concorrência ilimitados não criam capacidade; podem apenas acumular ou amplificar demanda.

A investigação mais eficiente não começa perguntando quanto hardware o servidor possui. Começa identificando qual recurso está impedindo o próximo trabalho útil de progredir. A partir daí, técnicas como isolamento, response projection, caching, request coalescing, bounded concurrency, backpressure, deadlines e load shedding deixam de ser otimizações isoladas e passam a formar uma estratégia coerente de resiliência.

Referências técnicas

[1] BASS, Len; CLEMENTS, Paul; KAZMAN, Rick. Software Architecture in Practice. 4. ed. Boston: Addison-Wesley Professional, 2022. Cap. 9: Performance. Disponível em: [https://www.sei.cmu.edu/library/software-architecture-in-practice-fourth-edition/](https://www.sei.cmu.edu/library/software-architecture-in-practice-fourth-edition/)

[2] BEYER, Betsy; JONES, Chris; PETOFF, Jennifer; MURPHY, Niall Richard (eds.). Site Reliability Engineering: How Google Runs Production Systems. Sebastopol: O’Reilly Media, 2016. Cap. 21: Handling Overload. Disponível em: [https://sre.google/sre-book/handling-overload/](https://sre.google/sre-book/handling-overload/)

[3] BEYER, Betsy; JONES, Chris; PETOFF, Jennifer; MURPHY, Niall Richard (eds.). Site Reliability Engineering: How Google Runs Production Systems. Sebastopol: O’Reilly Media, 2016. Cap. 22: Addressing Cascading Failures. Disponível em: [https://sre.google/sre-book/addressing-cascading-failures/](https://sre.google/sre-book/addressing-cascading-failures/)

[4] AMAZON WEB SERVICES. REL05-BP03: Control and limit retry calls. AWS Well-Architected Framework. Disponível em: [https://docs.aws.amazon.com/wellarchitected/latest/framework/rel_mitigate_interaction_failure_limit_retries.html](https://docs.aws.amazon.com/wellarchitected/latest/framework/rel_mitigate_interaction_failure_limit_retries.html)

[5] BROOKER, Marc. Exponential Backoff and Jitter. AWS Architecture Blog, 4 mar. 2015; atualização em maio de 2023. Disponível em: [https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)

Perguntas frequentes
Qual é a diferença entre concorrência e paralelismo?

Concorrência permite que múltiplas tarefas progridam em períodos sobrepostos, mesmo que um único executor alterne entre elas. Paralelismo exige execução simultânea em múltiplos recursos de processamento. Um sistema pode ser altamente concorrente sem executar todas as tarefas em paralelo.

CPU alta sempre significa que o servidor precisa de mais recursos?

Não. CPU alta pode ser causa ou consequência. O gargalo também pode estar em workers, filas, conexões, locks, banco de dados, memória ou serviços downstream. O diagnóstico deve localizar onde a latência cresce e qual recurso finito está saturado.

Por que aumentar o número de workers pode piorar a performance?

Mais workers aumentam a quantidade de trabalho concorrente enviada aos recursos compartilhados. Se o verdadeiro limite estiver no banco, storage ou outra API, ampliar o pool pode elevar contenção, filas e latência sem aumentar o throughput útil.

O que é backpressure em sistemas distribuídos?

Backpressure é o mecanismo pelo qual consumidores limitam a quantidade ou a velocidade de trabalho aceita dos produtores. Pode ser implementado com filas limitadas, throttling, limites de concorrência, quotas, rejeição antecipada, deadlines e load shedding.

Cache e request coalescing são a mesma coisa?

Não. Cache reutiliza um resultado previamente calculado. Request coalescing coordena chamadas simultâneas equivalentes para que uma única operação produza o resultado compartilhado. A combinação evita que vários cache misses concorrentes provoquem cache stampede.

Por que retries podem causar falhas em cascata?

Quando uma dependência já está sobrecarregada, retries adicionam novas requisições ao tráfego original. Sem limites, backoff, jitter e uma política consistente entre camadas, as tentativas podem se multiplicar e impedir a recuperação do serviço.

Materiais técnicos complementares

Soluções relacionadas

Serviços relacionados

Conteúdos principais sobre o tema

Conteúdos técnicos correlatos