A ligação em espera
Você liga para o suporte e a musiquinha começa. Se você se recusar a desligar, a linha fica ocupada — e ninguém mais da sua casa consegue usar o telefone. Não é o suporte que travou a sua casa: foi a sua decisão de esperar para sempre. Desligar depois de dez minutos não resolve o problema deles, mas devolve o telefone para você.
Lento é pior que morto
Quando uma dependência cai, você recebe connection refused em milissegundos e segue a vida. Quando ela apenas trava, você espera — e a espera custa uma thread, uma conexão e uma vaga no pool, cada uma delas indisponível para os pedidos que estavam saudáveis.
É assim que uma falha em um serviço vira uma falha em todos: o esgotamento de recursos se propaga para cima, e o serviço que cai não é o que quebrou.
O nome disso é exaustão de pool. Suponha um pool de 100 conexões e uma dependência que passou a responder em 30 segundos em vez de 50 milissegundos. Com 10 requisições por segundo, em dez segundos as 100 vagas estão ocupadas esperando — e a partir daí toda requisição nova, inclusive as que nem tocam nessa dependência, fica na fila do pool. O painel mostra o seu serviço com p99 explodindo, e a causa está a duas camadas de distância.
Por isso o timeout é o primeiro padrão da família de resiliência, e não um detalhe de configuração: ele é a fronteira que impede o problema do vizinho de virar o seu. Retry, Circuit Breaker e Bulkhead pressupõem que existe um prazo — sem ele, não há evento de falha para reagir.
Há um detalhe que quase todo mundo erra na primeira vez: a maioria das bibliotecas chama de *timeout* o tempo ocioso, não o tempo total. Um servidor que envia um byte a cada 1,9 segundo nunca estoura um read timeout de 2 segundos, e mantém a conexão presa por horas. É o ataque *slowloris*, e ele acontece por acidente com mais frequência do que por malícia.
Com prazo
- A chamada é abortada ao estourar o limite
- A conexão volta para o pool imediatamente
- A falha vira um erro nomeado, tratável no código
- O resto do sistema continua atendendo
O prazo não conserta a dependência lenta — ele impede que a lentidão dela vire indisponibilidade sua.
O prazo é do pedido, não da chamada
Configurar timeout por chamada parece suficiente e não é. Se o seu handler faz três chamadas de 1 segundo cada, o cliente que tinha 2 segundos de paciência já foi embora antes da terceira começar.
A forma correta é orçamento: o pedido entra com um tempo total, cada etapa consome uma parte, e o que sobra é o prazo da etapa seguinte.
Sem orçamento, os prazos se somam de um jeito que ninguém previu. Três chamadas de 1s parecem 1s de latência no desenho e são 3s no pior caso — e se cada uma tiver retry, viram 9s. O número que importa é o prazo do cliente lá na ponta, e ele precisa ser decrescente conforme desce a pilha.
A regra prática: o timeout de quem chama tem que ser menor que o de quem chamou. Se o gateway espera 5s e o serviço interno espera 10s, o serviço interno vai continuar trabalhando por mais 5 segundos numa resposta que ninguém vai ler — gastando banco, CPU e conexão em trabalho garantidamente inútil.
É por isso que sistemas maduros propagam o prazo restante no cabeçalho da requisição: gRPC tem grpc-timeout, e o context.Context do Go carrega o deadline explicitamente. Quando a etapa recebe o prazo em vez de configurá-lo, o orçamento fica correto por construção.
Como isso aparece na prática
O checkout que caiu por causa do serviço de recomendação
Cenário
A página de checkout busca recomendações de produto para exibir num carrossel lateral. O serviço de recomendação teve um problema de índice e passou a responder em 40 segundos em vez de 80 milissegundos.
Como o padrão entra
A chamada de recomendação recebe um prazo de 200ms, e o carrossel vira um estado vazio quando o prazo estoura. O checkout não depende dela para funcionar.
Trade-off: Alguns usuários deixam de ver recomendações durante o incidente — em troca de todos continuarem conseguindo comprar. Escolher o que é essencial exige decisão de produto, não só de engenharia.
A consulta que travou a tabela na Black Friday
Cenário
Um relatório administrativo roda uma consulta sem índice adequado. Em volume normal ela leva 2 segundos; sob o volume de pico, passa a levar 4 minutos e segura locks que a escrita precisa.
Como o padrão entra
statement_timeout no papel de banco usado pelo relatório mata a consulta em 10 segundos. O relatório falha; as vendas continuam.
Trade-off: O relatório passa a falhar em horário de pico e precisa de outra solução (réplica de leitura, pré-agregação). O timeout não resolve o problema — ele escolhe quem paga por ele.
Onde isto dá errado
Confundir tempo ocioso com tempo total
A maioria das bibliotecas chama de timeout o intervalo máximo entre bytes, não a duração total da chamada. Um servidor que envia um byte a cada 1,9 segundo nunca estoura um read timeout de 2 segundos e prende a conexão por horas. Sempre imponha também um prazo total.
Timeout de quem chama maior que o de quem chamou
Se o gateway desiste em 5 segundos e o serviço interno só desiste em 10, o serviço interno passa 5 segundos produzindo uma resposta que ninguém vai ler — gastando banco e CPU em trabalho garantidamente descartado. Os prazos precisam decrescer conforme se desce a pilha.
Timeout sem cancelamento de verdade
Abortar a espera não é o mesmo que abortar o trabalho. Se você para de esperar mas a query continua rodando no banco, o recurso que você queria liberar continua ocupado — você só deixou de saber disso. O cancelamento precisa chegar até a ponta.
Prazo escolhido pela média
Timeout dimensionado pela latência média corta metade das requisições legítimas assim que a carga sobe. O prazo se define pelo percentil alto (p99) mais uma folga, e sempre abaixo do que o cliente da ponta está disposto a esperar.
Estourar o prazo e não saber se o trabalho aconteceu
Um timeout num pedido de escrita é ambíguo: pode ser que ele não tenha chegado, ou que tenha sido executado e só a resposta se perdeu. Quem estoura prazo em operação que muda estado precisa de idempotência para poder tentar de novo sem duplicar.
A régua que justifica o prazo
Mexa nos controles: a teoria acima vira comportamento observável.
Quantas vezes cabe?
São Paulo ↔ Virgínia, ida e volta é 1,2 milhões × mais lento que leitura da memória principal.
Se o ciclo de CPU durasse um segundo, a ida e volta até a Virgínia levaria mais de doze anos. É essa a distância que uma chamada de rede atravessa, e a razão de todo pedido precisar de prazo.
Na régua
Clique num ponto para comparar. Só o par escolhido leva rótulo.
Cada linha de grade é 10× a anterior. É o único jeito de tudo caber.
usado pelo simulador do Construtorreferência de hardware e rede
Todos os pontos
Por ordem de grandeza. Clique para colocar no comparador.
Nanossegundos
Microssegundos
Milissegundos
Segundos
E se a dependência travar?
Mexa nos controles: a teoria acima vira comportamento observável.
Simulador de falha
O banco já está fora. Vai colocando proteção e olha o custo cair.
Proteger a borda
Sem prazo. A dependência trava e você segura a thread uns 30 segundos.
Latência sentida
30s
- 1Usuário · confirma a ação: POST /pedidos0ns
- 2API / Gateway · Nginx termina TLS e balanceia → encaminha2ms
- 3Aplicação (casos de uso) · caso de uso orquestra a operação2ms
- 4Domínio · invariantes de negócio validam o comando1ms
- 5Escrita falhou · PostgreSQL não responde: sem prazo, a chamada pendura até o cliente desistir e a conexão fica presa até lá30s
- 6Erro · a requisição falhou: o usuário recebe erro0ns
- Sem timeout, esta falha custa 30 segundos de conexão presa. Com prazo configurado seriam 2 segundos; com um disjuntor aberto, microssegundos. A dependência quebrada é a mesma; o que muda é o que protege a borda.
- Sem fila para absorver a escrita, a queda do banco vira erro imediato. Uma fila (Kafka/RabbitMQ) transformaria isso em 'aceito, processo depois'.
Prazo por chamada e orçamento por pedido
// Sem timeout: esta chamada pode ficar pendurada para sempre.
// O servidor nao precisa cair — basta ele aceitar a conexao e nunca responder.
const resposta = await fetch(url);
// Com timeout: um limite explicito, e o recurso e liberado.
async function buscarComPrazo(url: string, ms: number) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), ms);
try {
return await fetch(url, { signal: ctrl.signal });
} finally {
clearTimeout(timer); // sem isto, o timer segura o processo vivo
}
}
// O orcamento e do PEDIDO, nao da chamada: se ja gastei 800ms,
// a proxima chamada nao pode ter 1000ms de prazo.
async function comOrcamento<T>(restanteMs: number, fn: (ms: number) => Promise<T>) {
if (restanteMs <= 0) throw new Error("orcamento esgotado");
return fn(restanteMs);
}import httpx
# Sem timeout, o httpx espera indefinidamente por padrao em alguns transportes.
# Pior: 'timeout' costuma significar tempo OCIOSO, nao tempo total.
async with httpx.AsyncClient(
timeout=httpx.Timeout(
connect=1.0, # abrir a conexao
read=2.0, # esperar cada pedaco de resposta
write=2.0,
pool=1.0, # esperar uma conexao livre no pool
)
) as cliente:
r = await cliente.get(url)
# Um servidor que manda 1 byte a cada 1.9s nunca estoura o 'read' de 2s
# e mantem a conexao presa por horas. Para isso existe o prazo TOTAL:
import asyncio
async def com_prazo_total(coro, segundos: float):
return await asyncio.wait_for(coro, timeout=segundos)Quando usar × quando evitar
Use quando
3- Toda chamada que atravessa um limite de processo — rede, banco, disco, fila.
- Ao adquirir recurso escasso: conexão de pool, lock distribuído, semáforo.
- Como orçamento decrescente ao longo da cadeia de chamadas.
Evite quando
3- Como remendo para lentidão crônica — o prazo esconde o sintoma, não corrige a causa.
- Em trabalho de fundo legitimamente longo, onde o certo é acompanhar progresso.
- Com valor escolhido pela latência média em vez do percentil alto.
Testar o que ficou
5 armadilhas de Timeout viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
AbortController + fetchO jeito padrão do browser de cancelar uma requisição que passou do prazo, liberando a conexão.asyncio.wait_forImpõe prazo total a qualquer corrotina do Python, e não só ao tempo ocioso entre bytes.statement_timeout do PostgresO banco mata a consulta que passar do limite, protegendo-se de query que trava a tabela.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Todo pedido que sai da maquina tem prazo.
await fetch(url, { signal: AbortSignal.timeout(2000) });O que ele cobra
Nenhum salto a mais entre a chamada e o efeito.
- Um número a calibrar por chamada, que envelhece quando o tráfego muda
- Requisições legítimas e lentas passam a falhar
Não vale se: nunca. Não existe caso legítimo de chamada de rede sem prazo — o que varia é o valor.
Onde isto já custou caro
Incidentes reais que este conceito explica.
- Amazon Web ServicesUm comando digitado errado tira metade da internet do arBoa parte dos serviços afetados não caiu por erro do S3, e sim por esgotar pool esperando resposta que não vinha. Lento é pior que morto.
- CloudflareUma expressão regular consome 100% da CPU globalNão havia prazo para a execução de uma regra. Um limite de tempo por avaliação teria transformado a regex ruim num erro contido, em vez de um consumo ilimitado de CPU.
Onde isto aparece
Continue por aqui
- Retry com backoff e jitterFalhas passageiras se resolvem sozinhas se você esperar um pouco e tentar de novo. Mas repetir do jeito errado — sem espera crescente, sem ruído aleatório, sem filtrar o que é repetível — transforma um soluço em uma queda.
- Circuit BreakerQuando uma dependência já está claramente fora do ar, continuar chamando não ajuda ninguém: gasta o seu prazo e atrapalha a recuperação dela. O disjuntor detecta o padrão de falha e passa a recusar na hora, até que valha a pena tentar de novo.
- IdempotênciaExecutar a mesma operação duas vezes tem o mesmo efeito de executar uma: é o que torna o retry seguro. Em rede, onde 'não sei se foi' é resposta comum, é a diferença entre reenviar e cobrar em dobro.