Boas práticas de gestão de timeouts em chamadas a serviços externos

1. Por que timeouts são críticos para a resiliência do sistema

Em sistemas distribuídos modernos, uma chamada a serviço externo sem timeout é como uma granada sem pino. O impacto pode ser devastador: threads bloqueadas indefinidamente, conexões esgotadas e degradação em cascata que derruba todo o ecossistema.

Considere um cenário típico: um serviço A chama o serviço B, que por sua vez chama o serviço C. Se C fica lento e A não tem timeout, as threads de A acumulam-se, consumindo memória e CPU. Quando o pool de threads esgota, A para de responder — e todos os serviços que dependem de A também falham. É o efeito dominó em ação.

Existem três tipos fundamentais de timeout:

  • Timeout de conexão: tempo máximo para estabelecer a conexão TCP
  • Timeout de leitura: tempo máximo entre pacotes de dados recebidos
  • Timeout total: tempo máximo para a operação completa (conexão + envio + resposta)

O princípio de fail fast (falha rápida) preconiza que é melhor falhar rapidamente do que esperar indefinidamente. Um timeout bem calibrado transforma uma falha silenciosa em uma resposta clara e gerenciável.

2. Definindo valores de timeout com base em métricas reais

Definir timeouts no chute é receita para desastre. A abordagem correta usa percentis de latência observada:

# Exemplo de cálculo de timeout baseado em percentis
p50 = 150ms  (mediana)
p95 = 800ms  (95% das requisições completam em 800ms)
p99 = 2000ms (99% completam em 2s)

Timeout sugerido = p99 + margem de segurança (20-30%)
Timeout final = 2000ms * 1.3 = 2600ms

Timeouts muito curtos geram falsos positivos — requisições legítimas que falham por pouco. Timeouts muito longos mascaram problemas de latência e escondem degradação de desempenho.

Estratégias avançadas incluem timeout adaptativo, onde o valor é ajustado dinamicamente com base na latência histórica:

timeout_atual = media_movel(p95_ultimos_5_minutos) * fator_seguranca
fator_seguranca = 1.5 (configurável por ambiente)

3. Implementação de timeouts em diferentes camadas

Chamadas HTTP

// Exemplo com axios (Node.js)
const axios = require('axios');

const cliente = axios.create({
  timeout: 5000,           // timeout total
  timeoutErrorMessage: 'Serviço externo não respondeu em 5s'
});

// Timeout específico por requisição
try {
  const resposta = await cliente.get('/api/dados', {
    timeout: {
      connection: 2000,    // timeout de conexão
      read: 3000           // timeout de leitura
    }
  });
} catch (erro) {
  if (erro.code === 'ECONNABORTED') {
    console.error('Timeout atingido:', erro.message);
  }
}

Filas de mensagens

// RabbitMQ com TTL na fila
channel.assertQueue('fila_processamento', {
  arguments: {
    'x-message-ttl': 30000,      // 30 segundos de TTL
    'x-dead-letter-exchange': 'dlx_processamento'
  }
});

// Kafka com max.poll.interval.ms
consumer = new KafkaConsumer({
  'max.poll.interval.ms': 300000,  // 5 minutos
  'session.timeout.ms': 45000       // 45 segundos
});

gRPC com deadlines

// Go com contexto e deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

resposta, err := cliente.ChamarServico(ctx, &requisicao)
if err == context.DeadlineExceeded {
    log.Warn("Timeout na chamada gRPC")
}

4. Padrões de fallback e retry com timeout inteligente

Retry sem controle é um tiro no pé. O padrão correto combina backoff exponencial com jitter para evitar tempestade de retries:

tentativa = 0
max_tentativas = 3
base_delay = 100ms

enquanto tentativa < max_tentativas:
    try:
        resposta = chamar_servico(timeout=2000ms)
        return resposta
    except TimeoutError:
        tentativa++
        delay = base_delay * (2 ** tentativa)  # backoff exponencial
        delay += random(0, delay * 0.1)         # jitter de 10%
        sleep(delay)

# Após todas tentativas, acionar fallback
return cache_local.get('dados_fallback') ?? resposta_padrao

O Circuit Breaker integra-se perfeitamente com timeouts:

// Se timeouts excedem 50% em uma janela de 10 segundos
// o circuito abre e todas as chamadas falham imediatamente
circuit_breaker_config = {
    'threshold': 0.5,        // 50% de falhas
    'window_ms': 10000,      // janela de 10 segundos
    'timeout_ms': 3000       // timeout individual
}

5. Monitoramento e alertas para timeouts

Métricas essenciais que todo sistema deve coletar:

# Métricas de timeout
timeout_rate = timeouts / total_requisicoes
latency_p50 = percentil(50, latencias)
latency_p99 = percentil(99, latencias)

# Log estruturado para debugging
{
  "event": "timeout_occurred",
  "service": "payment-gateway",
  "endpoint": "/charge",
  "configured_timeout_ms": 5000,
  "actual_duration_ms": 5230,
  "thread_id": "http-nio-8080-exec-12",
  "trace_id": "abc123def456"
}

Alertas inteligentes baseiam-se em desvio de baseline, não em valores absolutos:

ALERTA: timeout_rate > media_historica + 3*desvio_padrao
DURAÇÃO: 5 minutos consecutivos
AÇÃO: Notificar equipe de plantão

6. Tratamento de timeouts em arquiteturas assíncronas

Em sistemas baseados em eventos, timeouts precisam de tratamento especial:

# Fila com dead letter queue (DLQ)
config_fila_eventos = {
    'exchange': 'pedidos.exchange',
    'queue': 'pedidos.processamento',
    'ttl': 60000,                    # 1 minuto de TTL
    'dlq': 'pedidos.timeout.dlq',    # mensagens vão para DLQ
    'max_retries': 3
}

# SAGA com timeout por etapa
saga_pedido = [
    {'etapa': 'reservar_estoque', 'timeout': 5000},
    {'etapa': 'processar_pagamento', 'timeout': 10000},
    {'etapa': 'enviar_email', 'timeout': 3000}
]

# Se qualquer etapa excede o timeout, a SAGA dispara rollback

Para tarefas em background:

# Python com asyncio
async def processar_tarefa():
    try:
        async with asyncio.timeout(30):
            resultado = await servico_externo.processar()
            return resultado
    except asyncio.TimeoutError:
        cancelar_tarefa_background()
        registrar_timeout()
        return None

7. Testes de resiliência para validar timeouts

Testes de caos revelam comportamentos inesperados:

# Simulação de latência com Toxiproxy
toxiproxy-cli create slow_service -l localhost:3000 -u external:4000
toxiproxy-cli toxic add slow_service -t latency -a latency=5000

# Teste: serviço externo agora responde em 5s
# Timeout configurado em 3s → deve falhar rapidamente

Testes de integração com mocks lentos:

# Mock que simula timeout
class ServicoLentoMock:
    async def chamar(self):
        await asyncio.sleep(10)  # Simula latência
        return {"status": "ok"}

# Verificação sob carga
resultado = await sistema_sob_teste.chamar_servico()
assert resultado.status == "timeout"
assert resultado.tempo_resposta < 3000

Conclusão

Timeouts não são meras configurações técnicas — são contratos de resiliência entre serviços. Uma gestão madura de timeouts envolve:

  1. Calibração baseada em dados (percentis de latência real)
  2. Implementação em múltiplas camadas (conexão, leitura, total)
  3. Padrões de retry e fallback com backoff inteligente
  4. Monitoramento contínuo com alertas contextuais
  5. Testes de resiliência que validam o comportamento sob estresse

Lembre-se: em sistemas distribuídos, a ausência de timeout é uma dívida técnica que sempre será cobrada — com juros compostos de degradação em cascata.


Referências