Serviços externos, bancos de dados, filas e APIs podem falhar por alguns segundos e voltar a funcionar logo depois. Repetir uma operação pode resolver essas falhas temporárias, mas tentativas imediatas e ilimitadas aumentam a carga justamente quando o sistema está degradado. O retry com backoff no Node.js cria intervalos progressivos entre as tentativas e estabelece limites claros para evitar tempestades de requisições.
Uma política confiável precisa responder a várias perguntas: quais erros podem ser repetidos, quantas tentativas são aceitáveis, quanto tempo total o chamador pode esperar, como cancelar a operação e se a ação é idempotente. Sem essas decisões, o retry pode duplicar pagamentos, atrasar respostas e esconder indisponibilidades.
Neste guia, você aprenderá backoff exponencial, jitter, timeouts, classificação de erros, idempotência, integração com fetch(), observabilidade, testes e combinação com circuit breaker.
Quando uma tentativa deve ser repetida?
Retry é indicado para falhas provavelmente temporárias, como timeout de rede, conexão reiniciada, resposta HTTP 429, alguns erros 5xx e indisponibilidade momentânea de uma dependência. Erros de validação, autenticação inválida, recurso inexistente e regras de negócio normalmente não melhoram com uma nova tentativa.
Classifique explicitamente os erros:
function isRetryable(error) {
if (error.name === 'AbortError') return false;
if (error.status === 429) return true;
if (error.status >= 500 && error.status <= 599) return true;
return [
'ECONNRESET',
'ETIMEDOUT',
'EAI_AGAIN'
].includes(error.code);
}A classificação depende da operação. Um HTTP 500 pode representar falha temporária ou um erro permanente no payload. Use o contrato da API e métricas reais.
A documentação oficial de Timers Promises mostra esperas assíncronas canceláveis. O artigo da AWS sobre timeouts, retries e backoff com jitter explica por que clientes sincronizados podem amplificar uma falha.
Retry simples com limite
async function retry(operation, maxAttempts = 3) {
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation(attempt);
} catch (error) {
lastError = error;
if (!isRetryable(error) || attempt === maxAttempts) {
throw error;
}
}
}
throw lastError;
}O limite inclui a primeira execução. Três tentativas significam uma chamada original e até duas repetições. Nomear o parâmetro dessa forma evita ambiguidade.
Por que esperar entre as tentativas?
Se milhares de clientes repetirem imediatamente, o serviço recebe uma nova onda de tráfego antes de se recuperar. O backoff aumenta o intervalo a cada falha:
function exponentialDelay(attempt, baseMs = 200, maxMs = 5000) {
const delay = baseMs * 2 ** (attempt - 1);
return Math.min(delay, maxMs);
}Com base de 200 ms, os intervalos seriam 200, 400, 800 e 1600 ms. O teto impede esperas enormes.
Adicionando jitter
Backoff sem aleatoriedade ainda pode sincronizar clientes. O jitter distribui as tentativas:
function fullJitter(attempt, baseMs = 200, maxMs = 5000) {
const ceiling = Math.min(
maxMs,
baseMs * 2 ** (attempt - 1)
);
return Math.floor(Math.random() * ceiling);
}Math.random() é suficiente para distribuir tempo; não é usado como segredo. Outras estratégias incluem equal jitter e decorrelated jitter. Escolha uma e mantenha métricas para entender o comportamento.
Espera cancelável
import { setTimeout as delay } from 'node:timers/promises';
async function waitBeforeRetry(milliseconds, signal) {
await delay(milliseconds, undefined, { signal });
}O mesmo sinal usado pela operação deve cancelar a espera. Assim, a aplicação não continua dormindo depois que o cliente desconecta ou o processo inicia o encerramento. Consulte AbortController no Node.js.
Implementação completa
async function retryWithBackoff(operation, options = {}) {
const {
maxAttempts = 4,
baseDelayMs = 200,
maxDelayMs = 5000,
signal,
shouldRetry = isRetryable,
onRetry = () => {}
} = options;
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
signal?.throwIfAborted();
try {
return await operation({ attempt, signal });
} catch (error) {
lastError = error;
if (
signal?.aborted ||
!shouldRetry(error) ||
attempt === maxAttempts
) {
throw error;
}
const delayMs = fullJitter(
attempt,
baseDelayMs,
maxDelayMs
);
onRetry({ attempt, delayMs, error });
await waitBeforeRetry(delayMs, signal);
}
}
throw lastError;
}A função recebe decisões por parâmetro, facilitando testes e políticas diferentes para cada dependência.
Usando com fetch()
async function fetchJson(url, signal) {
return retryWithBackoff(async ({ signal }) => {
const response = await fetch(url, {
signal: AbortSignal.any([
signal,
AbortSignal.timeout(3000)
])
});
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
error.status = response.status;
throw error;
}
return response.json();
}, {
signal,
maxAttempts: 3
});
}Cada tentativa possui timeout próprio, enquanto o sinal externo representa o ciclo de vida total. Em sistemas críticos, adicione também um orçamento total para impedir que a soma de esperas ultrapasse o SLA.
Respeitando Retry-After
Respostas 429 e 503 podem incluir Retry-After. O valor pode ser segundos ou uma data HTTP:
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());
}Aplique um teto, pois o servidor pode informar uma espera incompatível com o tempo total da operação.
Idempotência
Uma tentativa pode falhar depois que o servidor já processou a ação, mas antes da resposta chegar. Repetir um pagamento ou criação sem proteção gera duplicidade. Use chaves de idempotência:
const idempotencyKey = crypto.randomUUID();
await fetch('/payments', {
method: 'POST',
headers: {
'idempotency-key': idempotencyKey,
'content-type': 'application/json'
},
body: JSON.stringify(payment)
});O servidor precisa armazenar a chave e devolver o mesmo resultado para repetições equivalentes. O guia de webhooks seguros com Node.js apresenta processamento idempotente.
Retry em bancos de dados
Não repita automaticamente uma transação inteira sem entender o erro. Deadlocks e falhas de serialização podem ser candidatos, mas uma conexão perdida após o commit deixa o resultado incerto. Consulte o estado ou use identificadores únicos antes de repetir.
Retry em filas
Filas normalmente possuem tentativas, atraso e dead letter queue. Evite implementar outra camada de retry dentro do consumidor sem coordenar as duas políticas. Caso contrário, uma única mensagem pode executar dezenas de vezes.
Orçamento total
Defina uma data limite:
function remainingTime(deadline) {
return Math.max(0, deadline - Date.now());
}Antes de uma nova tentativa, confirme que há tempo para espera e execução. Uma rota com timeout de cinco segundos não deve iniciar uma tentativa de três segundos aos 4,5 segundos.
Retry e Circuit Breaker
Retry lida com falhas esporádicas. Circuit breaker interrompe chamadas quando a dependência permanece indisponível. Juntos, eles evitam insistência excessiva. O retry deve acontecer dentro da chamada protegida ou conforme o desenho do breaker, sempre com limites consistentes.
Para reduzir pressão em APIs, combine também as técnicas de Rate Limiting em APIs Node.js e performance de APIs Node.js.
Observabilidade
Registre:
- dependência e operação;
- número da tentativa;
- tipo do erro;
- atraso escolhido;
- duração total;
- resultado final;
- se o limite foi atingido.
Não gere alerta crítico para cada tentativa individual. Alerte por taxa de retries, esgotamento e latência acumulada. Integre traces com OpenTelemetry no Node.js.
Como testar
Injete a função de espera e o gerador de jitter para tornar os testes determinísticos:
test('repete duas vezes e retorna sucesso', async () => {
let calls = 0;
const result = await retryWithBackoff(async () => {
calls++;
if (calls < 3) {
const error = new Error('temporário');
error.code = 'ETIMEDOUT';
throw error;
}
return 'ok';
}, {
maxAttempts: 3,
baseDelayMs: 0
});
assert.equal(result, 'ok');
assert.equal(calls, 3);
});Teste erro permanente, cancelamento durante a espera, teto, Retry-After, orçamento total e idempotência.
Erros comuns
- Repetir qualquer erro: validações e autenticação nunca melhoram.
- Não usar jitter: clientes voltam ao mesmo tempo.
- Retry ilimitado: a operação nunca termina.
- Ignorar idempotência: ações são duplicadas.
- Não cancelar a espera: o processo demora a encerrar.
- Empilhar políticas: cliente, fila e proxy multiplicam tentativas.
- Não definir timeout por tentativa: cada chamada pode travar.
- Esconder falhas com retry: a dependência permanece degradada sem alerta.
Boas práticas para produção
- Repita apenas falhas temporárias.
- Use poucas tentativas.
- Aplique backoff exponencial com jitter.
- Defina teto de atraso e orçamento total.
- Use timeout em cada tentativa.
- Propague AbortSignal.
- Respeite Retry-After com limite.
- Garanta idempotência.
- Coordene retries entre camadas.
- Monitore esgotamento e latência.
Conclusão
O retry com backoff no Node.js melhora a resiliência contra falhas breves sem sobrecarregar uma dependência degradada. Backoff exponencial, jitter e limites distribuem as chamadas e mantêm o tempo previsível.
Retry não corrige erros permanentes nem substitui idempotência, timeout e circuit breaker. Quando a política classifica erros, respeita o ciclo de vida da requisição e produz métricas, as repetições deixam de ser uma reação automática e passam a ser parte controlada da arquitetura.




