Retry é a repetição controlada de uma operação que falhou por uma condição transitória. Em aplicações Node.js, retries podem recuperar chamadas afetadas por reset de conexão, timeout breve, HTTP 429 ou indisponibilidade temporária. Porém, repetir sem critérios aumenta carga e pode transformar uma falha parcial em incidente.
Uma política segura combina classificação de erros, número limitado de tentativas, exponential backoff, jitter, deadline, idempotência e observabilidade.
Quando tentar novamente
Erros normalmente transitórios incluem:
- reset de conexão;
- falha temporária de DNS;
- timeout de conexão;
- HTTP 429;
- HTTP 502, 503 e 504;
- leader election ou failover;
- conflito transitório documentado.
Erros de autenticação, validação, permissão e recurso inexistente normalmente não melhoram com retry.
Idempotência
GET, HEAD e operações idempotentes são candidatas melhores. POST só deve ser repetido quando o contrato usa idempotency key ou outra deduplicação.
Exponential backoff
function exponentialDelay(attempt, {
baseMs = 100,
maxMs = 5000,
} = {}) {
return Math.min(maxMs, baseMs * 2 ** attempt);
}Sem backoff, todas as tentativas acontecem imediatamente e pressionam uma dependência já ruim.
Jitter
Se milhares de clientes usam o mesmo atraso, eles repetem juntos. Jitter distribui as tentativas.
function fullJitter(attempt, options) {
const cap = exponentialDelay(attempt, options);
return Math.random() * cap;
}Full jitter escolhe um valor entre zero e o limite exponencial.
Função genérica
import { setTimeout as sleep } from 'node:timers/promises';
async function retry(operation, {
attempts = 3,
signal,
shouldRetry = () => false,
baseDelayMs = 100,
maxDelayMs = 5000,
onRetry = () => {},
} = {}) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt += 1) {
signal?.throwIfAborted();
try {
return await operation({ attempt, signal });
} catch (error) {
lastError = error;
const hasNext = attempt + 1 < attempts;
if (!hasNext || !shouldRetry(error)) {
throw error;
}
const cap = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
const delayMs = Math.random() * cap;
onRetry({ attempt: attempt + 1, delayMs, error });
await sleep(delayMs, undefined, { signal });
}
}
throw lastError;
}A operação recebe o mesmo signal para respeitar cancelamento global.
Classificando HTTP
function isRetryableStatus(status) {
return status === 408
|| status === 425
|| status === 429
|| status === 502
|| status === 503
|| status === 504;
}O contrato da dependência pode exigir lista diferente. Não repita todo 500 automaticamente, pois pode ser bug determinístico.
Retry-After
Respostas 429 e 503 podem informar:
Retry-After: 120Ou uma data HTTP. O cliente deve respeitar o valor dentro do deadline e aplicar um limite máximo.
function parseRetryAfter(value) {
if (!value) return null;
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
const date = Date.parse(value);
if (Number.isNaN(date)) return null;
return Math.max(0, date - Date.now());
}Deadline total
Três tentativas de cinco segundos não cabem em uma requisição de oito segundos. Defina um deadline e calcule tempo restante.
const deadline = Date.now() + 8000;
function remainingMs() {
return Math.max(0, deadline - Date.now());
}Cada tentativa deve usar um timeout menor que o tempo restante e reservar margem para responder ao cliente.
Timeout por tentativa
const attemptSignal = AbortSignal.any([
globalSignal,
AbortSignal.timeout(Math.min(2000, remainingMs())),
]);Não deixe uma tentativa consumir todo o orçamento.
Fetch com retry
async function fetchWithRetry(url, options = {}) {
return retry(async ({ signal }) => {
const response = await fetch(url, {
...options,
signal,
});
if (isRetryableStatus(response.status)) {
const retryAfter = response.headers.get('retry-after');
await response.body?.cancel();
const error = new Error(`HTTP ${response.status}`);
error.status = response.status;
error.retryAfterMs = parseRetryAfter(retryAfter);
throw error;
}
return response;
}, {
attempts: 3,
signal: options.signal,
shouldRetry: (error) =>
isNetworkError(error) || isRetryableStatus(error.status),
});
}Uma implementação completa deve incorporar Retry-After no cálculo de atraso.
Consumir resposta antes do retry
Libere o corpo da resposta antes de tentar novamente para permitir reutilização da conexão.
Erros de rede
Classifique por propriedades estáveis da biblioteca. Mensagens de texto podem mudar. Exemplos conhecidos incluem resets, timeout e falhas temporárias de resolução.
POST com idempotency key
const key = crypto.randomUUID();
await retry(({ signal }) => fetch('/payments', {
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': key,
},
body: JSON.stringify(payment),
signal,
}), policy);A mesma chave deve ser usada em todas as tentativas.
Banco de dados
Retries de transações podem ser necessários em deadlock ou conflito de serialização. Reexecute a transação inteira, não apenas a última query. Limite tentativas e preserve idempotência de efeitos externos.
Mensageria
Filas normalmente oferecem retries e dead letter. Use backoff, limite máximo e deduplicação. Uma mensagem que sempre falha não deve bloquear a fila infinitamente.
Poison message
Erros determinísticos por payload inválido devem ir para dead letter rapidamente. Tentar centenas de vezes só consome recursos.
Retry storm
Um serviço com dez mil requisições por segundo e três tentativas pode produzir trinta mil chamadas. Combine com:
- circuit breaker;
- limite de concorrência;
- rate limiting;
- backpressure;
- fila limitada;
- load shedding.
Retry budget
Defina um orçamento global, por exemplo uma fração do tráfego normal. Quando retries excedem o budget, rejeite novas tentativas para proteger a dependência.
Circuit breaker
Quando a taxa de falha fica alta, o breaker abre e impede retries inúteis. A ordem precisa evitar que cada camada multiplique tentativas.
Retries em várias camadas
Se o gateway tenta três vezes, o serviço tenta três e o driver tenta três, uma chamada pode virar 27. Documente qual camada é responsável.
Hedged requests
Hedging inicia uma segunda leitura após um atraso curto e aceita a primeira resposta. Pode reduzir cauda de latência, mas aumenta carga. Use somente para operações idempotentes, com budget e cancelamento da chamada perdedora.
Fallback
Depois das tentativas, use fallback seguro: cache stale, resposta degradada ou fila. Não invente dados críticos.
Observabilidade
Registre e meça:
- tentativas por operação;
- sucesso após retry;
- falha final;
- atraso total;
- Retry-After;
- erro inicial e final;
- deadline esgotado;
- retries bloqueados pelo breaker;
- budget consumido.
Evite logar cada tentativa como erro grave. Use nível e amostragem adequados.
Tracing
Crie um span por tentativa ou eventos no span principal. Inclua número da tentativa e motivo, sem registrar credenciais.
Testes
Use relógio falso para testar atrasos sem esperar. Teste sucesso imediato, falha transitória, erro não repetível, Retry-After, abort, deadline, idempotency key e circuit breaker.
Chaos testing
Simule latência, reset, 429, 503 e falha parcial. Confirme que carga total permanece controlada.
Erros comuns
- repetir qualquer erro;
- não usar jitter;
- ignorar Retry-After;
- não definir deadline;
- repetir POST sem chave;
- multiplicar retries em camadas;
- não cancelar tentativa anterior;
- não consumir corpo;
- retry infinito em fila;
- não medir carga adicional.
Fluxo recomendado
Classifique erros, limite tentativas, use backoff com jitter, respeite Retry-After e propague o deadline. Combine com Idempotency Keys, Circuit Breaker, AbortController e Undici.
Consulte a referência de Retry-After e a especificação HTTP Semantics.




