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:
- Calibração baseada em dados (percentis de latência real)
- Implementação em múltiplas camadas (conexão, leitura, total)
- Padrões de retry e fallback com backoff inteligente
- Monitoramento contínuo com alertas contextuais
- 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
- Documentação oficial do Netflix Hystrix — Explica a integração entre timeout e circuit breaker em sistemas distribuídos
- Guia de resiliência do Microsoft Azure — Padrões de resiliência com timeout e retry para arquiteturas cloud
- Timeout e deadline no gRPC — Documentação oficial sobre deadlines e cancelamento de contexto em chamadas gRPC
- Artigo sobre timeout adaptativo do AWS — Estratégias de timeout adaptativo baseado em métricas de latência
- Testes de caos com Toxiproxy — Ferramenta para simulação de latência e falhas de rede em testes de resiliência
- Documentação do RabbitMQ sobre TTL e DLQ — Configuração de time-to-live e dead letter queues para filas de mensagens
- Guia de retry com backoff exponencial do Google — Boas práticas de retry com backoff e jitter para APIs externas