Uma dependência respondeu com erro. A reação mais comum é tentar de novo. Se a segunda tentativa funcionar, o usuário nem percebe a falha e o sistema parece mais resistente. O problema começa quando centenas de clientes, serviços intermediários e workers chegam à mesma conclusão ao mesmo tempo.
Nesse cenário, o retry deixa de ser uma rede de segurança e vira carga adicional justamente sobre o componente que já está lento. A falha pequena cresce, a fila aumenta, novas chamadas expiram e cada timeout dispara outra rodada de tentativas. É assim que uma instabilidade curta pode virar uma avalanche no backend.
Retry funciona, mas só para o tipo certo de falha
Retentar faz sentido quando a falha é transitória: uma conexão foi interrompida, uma instância reiniciou, o balanceador escolheu um destino indisponível ou o serviço respondeu com uma indicação temporária de sobrecarga. Nesses casos, uma nova chamada alguns instantes depois pode cair em um caminho saudável.
Já um erro de validação não vai melhorar com insistência. O mesmo vale para credenciais inválidas, recurso inexistente e regra de negócio recusada. Repetir essas chamadas apenas consome conexões, CPU e cota. Até respostas 5xx precisam de contexto: um 503 tende a ser temporário, enquanto repetir qualquer erro interno sem limite pode esconder um defeito permanente.
Também existe uma ambiguidade importante: timeout não significa necessariamente que a operação falhou. O cliente sabe apenas que não recebeu a resposta dentro do prazo. O servidor pode ter criado o pedido, debitado o pagamento ou publicado a mensagem antes de a conexão cair. Para operações com efeito colateral, retry seguro depende de idempotência ou de uma forma confiável de consultar o resultado anterior.
A multiplicação acontece entre camadas
Imagine uma requisição que passa por cinco serviços até chegar ao banco. Cada camada faz até três tentativas por conta própria. Se a consulta final estiver falhando, a combinação pode produzir até 35, ou 243 chamadas ao banco para atender uma única requisição original. Esse exemplo aparece na documentação de engenharia da Amazon porque mostra o perigo de uma política aparentemente razoável quando aplicada em todos os pontos da cadeia.
O desenho mais previsível escolhe uma camada responsável pela repetição, normalmente aquela que conhece melhor a operação e ainda tem tempo útil dentro do prazo total. As demais propagam o erro. Isso evita que bibliotecas HTTP, gateways, serviços e drivers façam retries invisíveis uns sobre os outros.
Timeout é orçamento, não cronômetro isolado
Cada tentativa precisa caber no prazo que ainda resta para a operação inteira. Se uma API promete responder em dois segundos, não adianta configurar três chamadas de dois segundos mais as esperas entre elas. Quando a última tentativa terminar, o cliente original provavelmente já terá desistido.
Vale trabalhar com um deadline compartilhado. A primeira tentativa recebe a maior parte do orçamento; as seguintes só acontecem se ainda houver tempo para concluir algo útil. O timeout também deve cobrir as etapas corretas, como resolução DNS, conexão, negociação TLS, envio e leitura. Configurar apenas a leitura pode deixar outras fases penduradas.
Backoff reduz a pressão; jitter evita o reencontro
Repetir imediatamente mantém a pressão sobre a dependência. O backoff exponencial aumenta o intervalo a cada falha: 200 ms, 400 ms, 800 ms e assim por diante, normalmente com um teto. Só isso ainda não resolve tudo. Se mil clientes falharam juntos e usam os mesmos intervalos, eles voltam juntos em ondas bem organizadas. É o famoso thundering herd.
O jitter adiciona aleatoriedade à espera para espalhar essas chamadas ao longo do tempo. Uma implementação simples de full jitter sorteia um valor entre zero e o teto calculado para aquela tentativa:
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchWithRetry(url, options = {}, config = {}) {
const {
maxAttempts = 4,
baseDelayMs = 200,
maxDelayMs = 5_000,
timeoutMs = 3_000,
} = config;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(timeoutMs),
});
const retryable = [429, 502, 503, 504].includes(response.status);
if (!retryable || attempt === maxAttempts - 1) {
return response;
}
} catch (error) {
if (attempt === maxAttempts - 1) throw error;
}
const ceiling = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
await sleep(Math.random() * ceiling);
}
}
O exemplo mostra a mecânica, não uma política universal. Em produção, o código ainda precisa considerar o deadline total, o cabeçalho Retry-After, cancelamento vindo do chamador, telemetria e se o método pode realmente ser repetido. SDKs maduros geralmente já implementam boa parte disso; criar uma segunda camada por cima sem conhecer os padrões da biblioteca pode reintroduzir a multiplicação.
Quais operações podem ser repetidas?
No HTTP, métodos seguros como GET e HEAD são idempotentes. PUT e DELETE também são definidos como idempotentes, embora possam produzir respostas diferentes entre tentativas. POST não recebe essa garantia por padrão. Isso não impede uma API de tornar uma operação POST repetível, mas o contrato precisa dizer como.
Uma chave de idempotência é a solução comum para pagamentos, criação de pedidos e outras operações em que duplicidade custa caro. O cliente envia um identificador estável para aquela intenção; o servidor registra o resultado e devolve a mesma operação quando a chave reaparece. Sem essa proteção, um timeout seguido de retry pode criar duas operações perfeitamente válidas do ponto de vista do servidor.
Retry também precisa de limite operacional
Limitar o número de tentativas é o começo. Sistemas maiores também usam um orçamento de retries: cada repetição consome uma cota que volta a crescer quando chamadas normais têm sucesso. Quando a cota acaba, o cliente falha rápido em vez de continuar alimentando um incidente. O modo padrão dos SDKs da AWS, por exemplo, usa um token bucket para esse controle.
Circuit breaker e isolamento de dependências podem complementar a estratégia. O primeiro interrompe chamadas temporariamente quando a falha passa de um limite; o segundo evita que uma integração lenta consuma todas as conexões ou toda a concorrência da aplicação. Nenhum dos dois substitui timeout, backoff ou capacidade suficiente no serviço chamado.
O que observar antes da próxima avalanche
- quantidade de tentativas por chamada original;
- taxa de sucesso depois do primeiro retry;
- tempo total consumido por espera e novas chamadas;
- respostas 429 e 503 separadas dos demais erros;
- volume de retries por dependência e por versão do cliente;
- operações duplicadas bloqueadas por idempotência;
- esgotamento de pools de conexão, threads ou concorrência.
Se quase nenhuma segunda tentativa funciona, a política está acrescentando latência sem recuperar disponibilidade. Se elas funcionam em períodos normais, mas explodem durante incidentes, faltam limite, backoff ou jitter. Retry não deve ser um reflexo espalhado pelo código. É uma decisão de capacidade e de contrato entre serviços.
A política precisa caber dentro de um prazo total
Três tentativas com timeout de cinco segundos não formam um timeout de cinco segundos. Acrescente DNS, conexão, espera no pool, backoff e trabalho local, e o request pode ultrapassar o limite do chamador. Defina um deadline absoluto e derive cada tentativa do tempo restante.
deadline = agora + 2s
enquanto tentativa < 3:
restante = deadline - agora
se restante < 100ms: pare
resposta = chamar(timeout = min(500ms, restante))
se sucesso ou erro permanente: retorne
espere jitter limitado pelo restante
Se o cliente cancela, propague o cancelamento para consultas e chamadas downstream. Continuar trabalhando depois que ninguém pode receber o resultado consome a capacidade de que o sistema precisa para se recuperar.
Status HTTP não decide sozinho
- 429: respeite
Retry-Aftere o orçamento do chamador. - 502, 503 e 504: podem ser transitórios, mas exigem limite e jitter.
- 400, 401, 403 e 422: repetir o mesmo pedido normalmente preserva o erro.
- POST com efeito: só repita quando o contrato oferece idempotência e reconciliação.
Uma operação que grava estado e publica mensagem pode combinar retries com transactional outbox. O outbox garante um registro recuperável; o retry entrega sem precisar refazer a transação de negócio.
