Como repetir requisições com backoff exponencial (retry)

Chamadas de rede falham de vez em quando: o servidor fica sobrecarregado, a conexão cai, a API pede para esperar. Repetir na hora piora o problema. Este guia mostra como repetir com backoff exponencial e jitter, quais respostas vale repetir (429 e 5xx, mas não 404) e como evitar que uma nova tentativa faça a mesma cobrança duas vezes.

Por Leonardo Gaertner · Publicado em

Por que esperar cada vez mais

Se mil clientes repetem a chamada na mesma hora em que o servidor caiu, ele volta e cai de novo. O backoff exponencial dobra a espera a cada tentativa (200 ms, 400 ms, 800 ms...), com um teto. O jitter sorteia a espera entre zero e esse limite, para os clientes não voltarem todos juntos.

O que vale repetir

  • Falhas de rede e tempo esgotado: podem passar na próxima tentativa.
  • 429 (muitas requisições) e 503 (indisponível): a API pede para esperar. Se a resposta trouxer o cabeçalho Retry-After, espere o tempo indicado.
  • Outros 5xx: costumam ser temporários.
  • Não repita 400, 401, 403, 404 e 422: a resposta não muda enquanto o pedido for o mesmo, e repetir só gasta tempo e cota.

Em JavaScript

A função comRetry serve para qualquer tarefa assíncrona, e buscarComRetry aplica as regras acima ao fetch. No fetch, uma falha de rede aparece como TypeError, enquanto respostas de erro, como 503, chegam normalmente e precisam ser conferidas pelo status.

JavaScript
const esperar = (ms) => new Promise((ok) => setTimeout(ok, ms));

// Repete a tarefa quando ela falha, esperando cada vez mais entre as tentativas
async function comRetry(tarefa, opcoes = {}) {
  const { tentativas = 5, baseMs = 200, maxMs = 5000, repetirSe = () => true } = opcoes;
  for (let i = 0; ; i++) {
    try {
      return await tarefa();
    } catch (erro) {
      if (i + 1 >= tentativas || !repetirSe(erro)) throw erro;
      // Backoff exponencial com jitter: espera um tempo aleatório entre 0 e o limite da vez
      const limite = Math.min(maxMs, baseMs * 2 ** i);
      await esperar(Math.random() * limite);
    }
  }
}

// Só vale repetir quando o problema pode passar: 429 (muitas requisições) e erros 5xx
async function buscarComRetry(url, opcoes) {
  return comRetry(async () => {
    const resposta = await fetch(url, opcoes);
    if (resposta.status === 429 || resposta.status >= 500) {
      throw Object.assign(new Error(`HTTP ${resposta.status}`), { temporario: true });
    }
    return resposta; // outros 4xx não mudam com nova tentativa
  }, { repetirSe: (e) => e.temporario || e.name === 'TypeError' }); // TypeError: falha de rede
}

Em Python e PHP

O repetir_se recebe o erro e decide se vale tentar de novo. Em Python, bibliotecas como tenacity e o Retry do urllib3 já fazem isso com mais opções.

Python
import random
import time


def com_retry(tarefa, tentativas=5, base=0.2, maximo=5.0, repetir_se=lambda erro: True):
    """Repete a tarefa quando ela falha, esperando cada vez mais entre as tentativas."""
    for i in range(tentativas):
        try:
            return tarefa()
        except Exception as erro:
            if i + 1 == tentativas or not repetir_se(erro):
                raise
            # Backoff exponencial com jitter: entre 0 e o limite da vez
            time.sleep(random.uniform(0, min(maximo, base * 2**i)))
PHP
<?php
// Repete a tarefa quando ela lança exceção, esperando cada vez mais entre as tentativas
function comRetry(
    callable $tarefa,
    int $tentativas = 5,
    int $baseMs = 200,
    int $maxMs = 5000,
    ?callable $repetirSe = null
) {
    for ($i = 0; ; $i++) {
        try {
            return $tarefa();
        } catch (Throwable $erro) {
            if ($i + 1 >= $tentativas || ($repetirSe && !$repetirSe($erro))) {
                throw $erro;
            }
            $limite = min($maxMs, $baseMs * 2 ** $i);
            usleep(random_int(0, $limite) * 1000); // jitter: entre 0 e o limite da vez
        }
    }
}

Não cobre duas vezes

Uma requisição pode chegar ao servidor e a resposta se perder no caminho. Se você repetir uma criação de pedido ou um pagamento, pode fazer tudo de novo. Repita sem medo só operações idempotentes, que dão o mesmo resultado se feitas duas vezes, como um GET ou um PUT que grava o mesmo valor.

  • Para criações e pagamentos, envie uma chave de idempotência: um identificador único por operação, no cabeçalho Idempotency-Key, que muitas APIs de pagamento aceitam. O servidor reconhece a repetição e devolve o resultado da primeira vez.
  • Gere essa chave uma vez, antes da primeira tentativa, por exemplo com um UUID, e mande a mesma em todas as repetições.
  • Limite o tempo total, não só o número de tentativas: o usuário não deve esperar um minuto por uma resposta.

Achou um erro neste guia? Avise por e-mail.

Perguntas frequentes

O que é backoff exponencial?

É dobrar o tempo de espera a cada nova tentativa, como 200 ms, 400 ms, 800 ms, até um limite. Assim o cliente insiste sem sobrecarregar um servidor que já está com problema.

Para que serve o jitter?

Para os clientes não tentarem todos ao mesmo tempo. Em vez de esperar exatamente o limite, cada um sorteia um tempo entre zero e o limite, e as novas tentativas se espalham.

Devo repetir uma requisição que deu 404?

Não. Erros 4xx, exceto o 429, indicam que o pedido em si está errado ou não é permitido, e a resposta será a mesma. Repita falhas de rede, 429 e 5xx.

Como evitar cobrança duplicada ao repetir um pagamento?

Envie uma chave de idempotência, gerada uma vez antes da primeira tentativa e repetida em todas. A API de pagamento reconhece a repetição e não cobra de novo.