Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

04 - Timeout e Deadlines

#deadline-propagation#latencia#p99#resiliencia#timeout
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Timeout e Deadlines

Timeout é o limite de tempo que uma chamada individual espera por resposta antes de desistir; deadline é o prazo absoluto que a operação completa, inclusive em cadeia, precisa respeitar antes de ser considerada inviável. Os dois conceitos formam a base invisível de todo sistema distribuído saudável, porque sem eles o comportamento padrão da rede é esperar indefinidamente por uma resposta que pode nunca chegar. Em produção, a ausência de timeout bem configurado é uma das causas mais frequentes de esgotamento de threads, pools e memória, e a ausência de deadline propagado é a razão pela qual cadeias de dez serviços somam latências que nenhum deles prometeu isoladamente. O padrão parece banal e não é: valores errados produzem tanto sucesso inútil, com resposta que chegou depois do prazo útil, quanto falha fabricada, com desistência antes do tempo razoável da dependência.

Conceito: timeout, deadline e orçamento de latência

Os três termos convivem e se complementam. Timeout é local e relativo: quanto tempo esta chamada, neste ponto do código, espera por resposta. Deadline é global e absoluto: o instante, medido no relógio monotônico compartilhado, em que a operação inteira precisa estar concluída, e ele viaja junto com a requisição pelos saltos da cadeia. Orçamento de latência é a divisão do deadline em fatias por componente, com folga, para que a soma das partes caiba no SLO prometido.

A distinção importa porque o timeout local sem deadline global cria o efeito de multiplicação: dez serviços em cadeia, cada um com timeout de três segundos, produzem uma experiência que pode custar trinta segundos ao usuário, embora cada componente esteja tecnicamente obediente. O deadline propagado resolve essa conta, porque cada salto conhece o tempo restante e restringe o próprio timeout à fração que ainda existe.

Nenhum desses valores deve ser escolhido por gosto. Eles derivam do SLO do endpoint, da distribuição de latência medida de cada dependência e da política de retry da chamada, em uma cadeia de raciocínio que começa na experiência prometida ao usuário e termina em uma constante de configuração.

O problema que resolve: espera infinita e acúmulo de recursos

Sem timeout, o comportamento padrão de bibliotecas de rede é esperar para sempre, ou por tempo enorme o suficiente para ser irrelevante na prática. Um banco de dados que trava sob lock, um serviço remoto em failover silencioso ou um firewall que descarta pacotes sem responder produzem chamadas pendentes que nunca terminam. Cada chamada pendente segura uma thread, uma conexão e memória; em poucos minutos o chamador esgota todos os recursos e deixa de servir até as funcionalidades saudáveis.

O segundo problema é o sucesso inútil. Uma resposta que chega em nove segundos para um usuário que desistiu no quarto não é sucesso, é desperdício duplo: o sistema gastou capacidade para processar uma requisição cujo resultado ninguém mais consumirá. O timeout estabelece o prazo de utilidade da resposta, e tudo que passa dele é carga morta que o sistema deveria abortar.

O terceiro problema é o erro incompleto em cadeia. Em arquiteturas de microserviços, a requisição do usuário atravessa gateway, orquestração e meia dúzia de serviços; se cada salto redefine o próprio timeout sem conhecer o resto do caminho, a latência total vira soma descontrolada, e os piores cenários multiplicam-se justamente nos picos, quando todas as dependências estão no seu p99 ao mesmo tempo.

Funcionamento interno: timeout por dependência

Todo cliente remoto precisa de um timeout configurado, e a configuração correta separa três fases da chamada: tempo para estabelecer conexão, tempo para enviar a requisição e tempo para receber a resposta. Conexão tem teto natural curto, porque estabelecer TCP ou TLS não deveria passar de poucas centenas de milissegundos na mesma rede; resposta tem teto derivado da distribuição de latência da dependência, tipicamente na região do p99 medido.

import http.client

# Timeouts separados por fase, no estilo das bibliotecas de cliente modernas
TIMEOUT_CONEXAO_S = 0.5   # estabelecer TCP/TLS: centenas de ms na mesma rede
TIMEOUT_RESPOSTA_S = 2.0  # derivado do p99 medido da dependência

def chamar_estoque(pedido_id: str) -> dict:
    conn = http.client.HTTPSConnection("estoque.interno",
                                       timeout=TIMEOUT_RESPOSTA_S)
    conn.connect()  # a fase de conexão usa o timeout de conexão
    try:
        conn.request("GET", f"/reservar/{pedido_id}")
        resposta = conn.getresponse()   # espera limitada pelo timeout de resposta
        return ler_json(resposta)
    except TimeoutError:
        # a desistência é local; o destino pode ainda executar o efeito,
        # por isso a operação exige chave de idempotência
        raise DependenciaForaDoPrazo("estoque não respondeu no prazo")
    finally:
        conn.close()

A leitura de distribuição é a parte que exige dados. Latência média esconde a cauda, e a cauda é que derruba sistemas: um serviço com média de 80 ms e p99 de 900 ms destrói um timeout de 500 ms em um por cento das chamadas, que em dez mil requisições por minuto é cem desistências fabricadas por minuto. O timeout de resposta precisa ficar acima do p99 da dependência saudável, e a diferença entre p99 e timeout é a margem de tolerância assumida.

Deadline propagation em chamadas em cadeia

O deadline propagado transforma o prazo em cidadão de primeira classe da requisição. O chamador original define o prazo absoluto e o envia em cabeçalho ou metadata; cada serviço intermediário lê o prazo, calcula o tempo restante e restringe todos os seus timeouts locais ao que resta. A infraestrutura de referência é o contexto do gRPC, que carrega deadline nativamente, e o padrão W3C de trace context, que transporta as mesmas informações em ambientes HTTP.

# Pseudocódigo de propagação de deadline no estilo gRPC
import time

def handler_pedido(req, prazo_recebido=None):
    # o chamador envia o prazo absoluto no cabeçalho da requisição
    prazo = prazo_recebido or time.monotonic() + 2.0  # default: 2 s para o pedido

    restante = prazo - time.monotonic()
    if restante <= 0:
        raise PrazoExcedido("orçamento consumido antes de começar")

    # cada dependência recebe apenas a fatia que ainda existe
    ctx_banco = contexto_com_prazo(prazo, fatia_ms=300)
    ctx_servico_b = contexto_com_prazo(prazo, fatia_ms=800)

    dados = banco.consultar(req.id, ctx=ctx_banco)
    resultado = servico_b.calcular(dados, ctx=ctx_servico_b)
    return responder(resultado)

O benefício aparece na cauda da distribuição: quando o serviço B está lento, o serviço A não espera o seu timeout cheio, porque percebe que o prazo global já não cabe, e aborta cedo com erro claro de prazo excedido. O aborto precoce libera threads, evita processamento de resultado inútil e torna a cadeia inteira honesta quanto ao tempo que ainda existe.

A regra complementar é a renovação de prazo no retry. Uma segunda tentativa dentro do mesmo prazo herda tempo insuficiente e falha por construção; clientes maduros reservam orçamento por tentativa ou renovam o prazo a cada nova execução, com a soma explícita documentada, porque orçamento invisível é orçamento rompido.

Orçamento de latência por endpoint

O orçamento de latência é o documento que divide o SLO do endpoint entre os componentes do caminho, com margem de segurança, e é a fonte da qual todos os timeouts derivam.

# Orçamento de latência do endpoint POST /pedidos, SLO de p99 em 800 ms
orcamento_latencia:
  total_slo_ms: 800
  componentes:
    - gateway: 30           # roteamento e autenticação no edge
    - servico_pedido: 120   # validação e montagem do agregado
    - banco_dados: 250      # consultas e escrita transacional
    - servico_estoque: 300  # chamada remota; a folga cobre um retry curto
    - serializacao: 100     # marshalling, logs e margem de segurança

A soma das fatias iguala o SLO, e a margem nunca é distribuída por igual: componentes com distribuição de cauda longa, como chamadas remotas, recebem fatia maior que componentes determinísticos, como serialização. O orçamento vive na documentação da equipe e vira alarme quando alguma dependência passa a consumir fatia própria com frequência, sinal de que o desenho precisa de cache, de fila ou de renegociação do SLO.

Interação entre timeout e retry

Timeout e retry formam o par mais traiçoeiro da resiliência, porque o erro clássico é o retry dentro de um orçamento que não existe: o cliente configura três tentativas de dois segundos em um endpoint cujo prazo total é de três segundos, e a segunda tentativa nasce morta, consumindo carga sem chance de sucesso.

Cenário Risco produzido Recomendação
Retry dentro do mesmo timeout local A nova tentativa herda tempo insuficiente e falha por construção Renovar prazo por tentativa ou reservar orçamento por execução
Timeout total menor que a soma das tentativas Retry declarado, mas matematicamente impossível Orçamento total igual ao timeout por tentativa vezes tentativas
Deadline propagado ausente na cadeia Cada salto espera o próprio total e a latência se multiplica Propagar prazo em todos os saltos e restringir timeouts locais a ele
Timeout maior que o SLO do usuário Sucesso técnico após prazo inútil para quem chamou Derivar timeout do orçamento do endpoint, não da tolerância do cliente
Timeout menor que o p99 da dependência Desistência fabricada em chamadas saudáveis Medir a distribuição e fixar timeout acima do p99, com margem

A ordem correta de raciocínio é de dentro para fora: primeiro o p99 medido da dependência, depois o timeout por tentativa, depois o número de tentativas, depois o prazo do endpoint. Inverter essa ordem, começando pelo desejo de muitas tentativas, produz a soma impossível que ninguém percebe até o incidente.

Derivando valores de métricas: p99

Valores de timeout começam na distribuição de latência medida, preferencialmente por percentil no histograma da dependência. A regra de partida é fixar o timeout entre o p99 e o p99.9 da dependência saudável, cobrindo a quase totalidade das chamadas legítimas e cortando a cauda que não vale esperar. Depois de fixado, o timeout precisa ser observado: taxa de desistência acima de um por cento em dependência saudável indica valor apertado demais, e taxa próxima de zero com p99 constante indica margem ociosa que pode ser devolvida ao orçamento.

Distribuição muda com o tempo, e o timeout configurado no primeiro mês não serve ao segundo. Deploys novos alteram perfis de latência, volume cresce, índices degradam; revisão periódica dos percentis por dependência, com alarme sobre desvios, transforma o timeout em parâmetro vivo. Malhas de serviço facilitam esse trabalho ao medir e aplicar timeouts de forma centralizada, mas a decisão de valor continua pertencendo a quem conhece o contrato de cada integração.

Bancos de dados merecem tratamento especial, porque expõem mecanismo de cancelamento real: o timeout do cliente que apenas fecha a conexão deixa a consulta rodando no servidor, consumindo recursos; statement timeout no lado do banco encerra a execução de fato. A combinação dos dois lados é a prática correta em qualquer consulta que possa degradar.

Valores típicos por tipo de chamada

Tipo de chamada Timeout típico Justificativa
Consulta em banco local 100 a 500 ms p99 de índice bem desenhado fica em milissegundos
Chamada gRPC interna 200 ms a 1 s Serviço leve na mesma rede, margem para fila curta
API REST interna 500 ms a 2 s Sobrecusto de HTTP e serialização no caminho
API externa de pagamento 2 a 5 s Prazos de mercado e compensação por idempotência
Chamada em função serverless Próximo do limite da função Esperar é gastar, pois o custo é por duração
Transferência de arquivo grande Minutos, com streaming Transporte de volume, não diálogo de requisição

Os números são ponto de partida e nunca substituem medição própria; a função da tabela é calibrar a expectativa inicial antes que existam métricas. O que não é ponto de partida é o valor ausente: nenhum cliente remoto deve chegar a produção sem timeout explícito em cada fase da chamada.

Erros comuns

  • Cliente remoto sem timeout configurado, que herda o padrão da biblioteca, às vezes minutos, e esgota threads na primeira lentidão do destino.
  • Timeout único global para todas as dependências, que trata cache local e API externa lenta com a mesma régua e fabrica tanto desistência quanto espera inútil.
  • Ausência de deadline propagado em cadeias longas, com cada serviço aplicando o próprio timeout sem conhecer o orçamento restante da operação.
  • Retry cuja soma de tentativas ultrapassa o prazo do endpoint, executando tentativas com matematicamente nenhuma chance de chegar a tempo.
  • Timeout definido pela média de latência da dependência, ignorando a cauda e cortando o percentil alto de chamadas perfeitamente saudáveis.
  • Cancelamento apenas no lado do cliente em consultas de banco, deixando a execução viva no servidor depois da desistência.
  • Prazo excedido tratado como erro genérico, sem distinção entre deadline excedido, timeout de conexão e timeout de resposta, o que destrói a triagem do incidente.

Boas práticas

  • Configurar timeout em toda chamada remota, separando as fases de conexão e de resposta, sem exceção nem para testes internos.
  • Derivar os valores do p99 medido por dependência e revisar periodicamente, com alarme sobre desvios da distribuição e sobre taxa de desistência.
  • Propagar deadline em todos os saltos da cadeia, com o mecanismo nativo da stack, e restringir todo timeout local ao prazo restante.
  • Montar orçamento de latência por endpoint, documentado, com fatia e margem por componente, e usá-lo como fonte única dos timeouts.
  • Combinar timeout com idempotência sempre que houver retry, porque desistência ambígua pode esconder operação executada no destino.
  • Cancelar de fato a execução no lado servidor quando a plataforma permite, com statement timeout no banco e cancelamento de contexto nas chamadas remotas.
  • Distinguir tipos de erro de prazo nas exceções e nos logs, com métricas separadas por fase, para que a observabilidade responda onde o tempo acabou.
  • Tratar mudança de timeout como decisão arquitetural revisável, registrada em configuração central e não em constantes espalhadas pelo código.