Timeout

TL;DR

A defesa mais simples da família e a mais esquecida — porque o default de muitas bibliotecas é esperar para sempre, e “para sempre” é a configuração que derruba sistemas. Sem timeout, uma dependência lenta retém suas threads e conexões até o pool acabar, e a lentidão dela vira indisponibilidade sua. É pré-requisito de todos os outros padrões: retry, circuit breaker e bulkhead pressupõem que uma tentativa termina. E há uma versão adulta do padrão que quase ninguém implementa: propagar o prazo pela cadeia, em vez de cada serviço ter o seu isoladamente.

O recorte desta nota

Aqui o timeout como decisão de projeto e o que ele sacrifica. Como escolher o valor a partir de percentis observados, e como operá-lo, está em Operação 3-06 (“Timeout: o valor, não o padrão”).

A configuração que ninguém tomou

Você investiga um incidente e procura o timeout da chamada ao serviço de recomendação. Não encontra — não há nenhuma linha configurando isso.

Não é negligência de uma pessoa: é o default. Muitos clientes HTTP, drivers e SDKs vêm com timeout de leitura infinito ou muito alto, porque a biblioteca não tem como saber quanto é razoável no seu caso. O resultado é que a decisão mais importante da chamada — quanto tempo vale a pena esperar — nunca foi tomada por ninguém, e o sistema herdou “para sempre”.

E “para sempre” tem uma consequência mecânica. Cada requisição em espera ocupa recursos: uma thread num modelo bloqueante, uma conexão do pool, memória do contexto da requisição. Se a dependência responde em 8 segundos em vez de 80 milissegundos, cada requisição ocupa esses recursos por 100× mais tempo — e o pool, dimensionado para o comportamento normal, enche com o mesmo tráfego de sempre.

O timeout é o que transforma “minha dependência está lenta” em “algumas requisições falham” em vez de “meu serviço parou”.

A ideia: decidir quando desistir

Um timeout é uma aposta: depois de t, a probabilidade de a resposta ainda vir e ser útil é baixa o bastante para valer mais liberar o recurso.

Há mais de um relógio envolvido, e confundi-los é fonte de bug:

TipoO que limitaSintoma quando falta
Conexãoestabelecer a conexãotrava com host inalcançável ou rede particionada
Leitura / respostaesperar os dados depois de conectadoo caso desta nota — trava com dependência lenta
Total da requisiçãoo tempo da operação inteira, incluindo retriesuma operação “com timeout de 2s” leva 6s após 3 tentativas
Ocioso (pool)conexão parada no poolrecursos retidos sem uso

O terceiro é o que mais surpreende: configurar timeout por tentativa não limita o tempo total se houver retry. O usuário espera a soma, e o orçamento real precisa ser pensado para a operação inteira.

A versão adulta: propagar o prazo

Numa cadeia A → B → C, cada serviço normalmente tem seu próprio timeout, escolhido isoladamente. Isso produz o desperdício mais comum e menos notado dos sistemas distribuídos:


graph TD
    subgraph I["Timeouts independentes"]
        A1["A — timeout 2s"] -->|"chama"| B1["B — timeout 10s"]
        B1 -->|"chama"| C1["C — lento"]
        N1["A desistiu em 2s.<br/>B e C seguem trabalhando 8s<br/>por uma resposta que ninguém recebe."]
    end

    subgraph P["Deadline propagado"]
        A2["A — prazo: agora+2s"] -->|"passa o prazo restante"| B2["B — restam 1,9s"]
        B2 -->|"passa o restante"| C2["C — restam 1,8s"]
        N2["C sabe que não vale a pena começar<br/>algo que leva 3s. Cancela cedo."]
    end

    style N1 fill:#D0021B,color:#fff
    style N2 fill:#4A90D9,color:#fff
    style A2 fill:#4A90D9,color:#fff

Em vez de cada serviço perguntar “quanto eu espero?”, a requisição carrega quanto tempo ainda resta — e cada salto passa adiante o saldo. Quem recebe um prazo já vencido nem começa o trabalho.

É o modelo de deadline do gRPC (que propaga por padrão) e dos Context do Go; em HTTP, exige convenção própria (um header com o prazo restante) e disciplina para respeitá-la. O ganho é grande em sistemas com cadeias profundas: trabalho que ninguém vai usar deixa de ser feito, exatamente no momento em que o sistema está sob pressão.

O que se sacrifica

Requisições que teriam sucesso se esperassem mais. Toda vez que o timeout dispara numa chamada que responderia em seguida, você transformou lentidão em erro — e quem paga é o usuário daquela requisição específica.

Essa é a assimetria que torna a decisão interessante: o benefício do timeout é coletivo e invisível (o sistema continua no ar para todos), e o custo é individual e visível (aquele usuário viu um erro). Times sem cultura de resiliência tendem a subir timeouts depois de reclamações pontuais, até que o valor volta a ser efetivamente infinito — desfazendo a proteção uma reclamação por vez.

Sacrifica também previsibilidade do trabalho já iniciado. Desistir do lado do cliente não cancela o trabalho do lado do servidor, a menos que haja cancelamento explícito. Sem propagação de prazo, o efeito colateral pode ocorrer depois de você ter desistido — e é por isso que timeout e idempotência andam juntos: você não sabe se a operação aconteceu.

Armadilhas comuns

Confiar no default

O que acontece: nenhuma linha de configuração, e a suposição de que a biblioteca traz algo sensato. Sob lentidão, o pool enche e o serviço para. Por quê: o default de muitos clientes é infinito ou muito alto — a biblioteca não conhece seu caso e escolhe não limitar. Como evitar: trate timeout explícito como obrigatório em toda chamada remota, verificável em revisão. E confira o default de cada cliente que você usa, um por um: eles diferem entre si, e “achei que tinha” é a causa raiz frequente.

Timeout do chamador menor que o do chamado

O que acontece: A desiste em 2s, B continua trabalhando por mais 8s numa resposta que será descartada — retendo recursos de B exatamente quando o sistema está sob estresse. Por quê: cada serviço configura o seu isoladamente, sem visão da cadeia. Como evitar: os timeouts devem decrescer de fora para dentro, e o ideal é propagar o prazo. No mínimo, documente a cadeia e verifique a coerência quando um valor mudar.

Esquecer que retry multiplica o tempo total

O que acontece: a chamada tem timeout de 2s e 3 tentativas. O usuário espera até 6 segundos, e a operação “com timeout de 2s” estoura o orçamento da requisição inteira. Por quê: o timeout foi pensado por tentativa; a experiência do usuário depende do total. Como evitar: defina um orçamento total para a operação e faça o retry respeitá-lo — parar de tentar quando o prazo global acabar, mesmo que restem tentativas. É o assunto direto da próxima nota.

Como explicar em inglês

“Timeout is the simplest pattern here and the one most often missing, because a lot of clients default to waiting forever — and forever is the setting that takes systems down. Without it, a slow dependency holds your threads and connections until the pool is exhausted, so their latency becomes your outage. It’s also a prerequisite for everything else: retry, circuit breaking and bulkheads all assume an attempt terminates. Two things I’d check in any review. First, timeouts should decrease as you go deeper into the call chain — if the caller gives up at two seconds and the callee waits ten, the callee is doing eight seconds of work nobody will receive. Second, per-attempt timeouts don’t bound total time once you add retries, so you want an overall budget for the operation. The grown-up version of the pattern is deadline propagation: the request carries how much time is left, and a service that receives an expired deadline doesn’t even start.”

PTEN
tempo limitetimeout
prazo propagadodeadline propagation
orçamento da requisiçãorequest budget
esgotamento de poolpool exhaustion
cancelamentocancellation
percentil (p99)percentile

O que vem a seguir

Definido que a espera termina, aparece a pergunta seguinte: quando a tentativa falha, vale tentar de novo? Às vezes sim — e é o único padrão desta família que, mal configurado, piora o incidente que deveria conter.

Veja também

  • Resiliência operacional — como escolher o valor a partir de percentis reais.
  • gRPC — o modelo de deadline propagado por padrão.
  • Idempotência — por que desistir sem saber o resultado exige operação idempotente.

Fontes

  • Michael NygardRelease It! (2ª ed., 2018) — timeouts como stability pattern, e o antipadrão das threads bloqueadas.
  • MicrosoftCloud Design Patterns — o catálogo de referência da família.
  • Google SRE BookAddressing Cascading Failures — o papel do timeout na contenção da cascata.
  • gRPCDeadlines — a formulação de prazo propagado em vez de timeout local.