A porta giratória lotada
O prédio evacuou e todos correm para a porta. Se cada pessoa que não conseguiu passar tentar de novo exatamente cinco segundos depois, o tumulto se repete idêntico, de cinco em cinco segundos, para sempre. Se cada uma esperar um tempo diferente e crescente, a fila se dissolve sozinha. A porta é a mesma — o que muda é a coordenação de quem espera.
Por que repetir ingenuamente piora tudo
Um serviço sob estresse precisa de folga para se recuperar. Retry sem backoff faz o contrário: no instante em que ele começa a falhar, a carga sobre ele aumenta, porque cada cliente agora manda duas ou três requisições no lugar de uma.
E sem jitter, todas as tentativas chegam no mesmo milissegundo — a falha sincronizou os clientes, e eles passam a bater em uníssono.
O nome disso é *thundering herd*, e o mecanismo é sutil: em operação normal, os pedidos dos clientes chegam distribuídos ao longo do tempo por pura falta de coordenação. Uma queda momentânea coordena todo mundo — todos falham no mesmo instante, todos esperam o mesmo intervalo, todos voltam juntos. A falha virou um metrônomo.
É por isso que o jitter importa mais que o backoff. Backoff exponencial sem ruído mantém as ondas, só mais espaçadas: 1s, 2s, 4s — mas ainda todos ao mesmo tempo. O ruído aleatório é o que devolve a distribuição que existia antes da falha.
A variante recomendada é a chamada *full jitter*: em vez de somar um ruído pequeno ao intervalo exponencial, sorteia-se dentro do intervalo inteiro, de zero até o teto exponencial. Espalha melhor e, contraintuitivamente, termina mais rápido no agregado — porque algumas tentativas acontecem cedo e encontram o serviço já recuperado.
Há também o efeito multiplicativo entre camadas. Se o gateway repete 3 vezes, o serviço repete 3 vezes e o cliente do banco repete 3 vezes, uma única requisição do usuário pode virar 27 no banco. Retry se configura em uma camada — normalmente a mais próxima da dependência que falha —, não em todas.
- Falha passageira
- erroÉ repetível?
- sim (503, 429, timeout)Espera exponencial + jitter
- dorme um tempo sorteadoNova tentativa
- ainda tem prazo?Teto de tentativas / orçamento
Nem toda falha é repetível
Retry só faz sentido quando a mesma requisição, repetida, pode dar outro resultado. 503 e 429 podem; 400 e 404 não vão mudar de ideia.
Repetir erro de cliente é gastar recurso para receber o mesmo não três vezes — e, sob incidente, é carga que atrapalha quem tinha chance.
A classificação certa é por natureza do erro, não por código de status apenas. Um 500 pode ser um bug determinístico (nunca vai melhorar) ou uma indisponibilidade momentânea (vai). Na dúvida, a heurística prática é: 408, 429, 502, 503, 504 e erros de rede/timeout são repetíveis; o resto da família 4xx não é.
O 429 merece atenção especial: ele quase sempre vem com um Retry-After. Ignorar esse cabeçalho e usar o seu próprio backoff é repetir contra a vontade explícita de quem sabe quando estará pronto — e é um jeito rápido de ser bloqueado por uma API externa.
Escritas exigem um cuidado a mais. Um timeout numa escrita é ambíguo: pode ser que o pedido não tenha chegado, ou que tenha sido executado e só a resposta se perdeu. Repetir sem chave de idempotência é como cobrar o cartão de novo porque a tela não carregou.
Como isso aparece na prática
O failover de banco que ninguém percebeu
Cenário
O banco gerenciado faz failover para a réplica. Por cerca de 8 segundos, as conexões existentes morrem e as novas são recusadas. Sem retry, todas as requisições nesse intervalo viram erro para o usuário.
Como o padrão entra
Retry com base de 100ms, teto de 3s e full jitter, limitado a 4 tentativas, cobre a janela do failover sem que ninguém veja erro.
Trade-off: A latência do p99 piora durante o failover, e o pool fica mais pressionado — em troca de o incidente ficar invisível para o usuário. É uma troca de latência por disponibilidade, e ela precisa caber no orçamento do pedido.
A integração que virou ataque acidental
Cenário
Um job noturno sincroniza 50 mil registros com uma API externa. A API começa a devolver 429, e o cliente HTTP repete imediatamente, três vezes, para cada registro.
Como o padrão entra
Passar a respeitar o Retry-After, adicionar jitter e limitar a concorrência do job resolve — e o backoff exponencial faz o job se adaptar sozinho ao limite da API.
Trade-off: O job passa a demorar mais e precisa tolerar execução parcial. A alternativa que parecia mais rápida estava, na prática, garantindo o bloqueio da chave de API.
Onde isto dá errado
Backoff sem jitter
Esperar 1s, 2s, 4s mantém todos os clientes sincronizados, só que em ondas mais espaçadas. A falha coordenou o rebanho, e sem ruído aleatório ele continua batendo em uníssono. O ruído é a parte que importa, não o crescimento.
Repetir em todas as camadas
Gateway com 3 tentativas, serviço com 3 e cliente de banco com 3 transformam uma requisição do usuário em 27 no banco. As tentativas se multiplicam, não se somam. Configure retry em uma camada só — a mais próxima da dependência que falha.
Repetir escrita sem idempotência
Timeout numa escrita é ambíguo: o pedido pode ter sido executado e só a resposta ter se perdido. Repetir sem chave de idempotência é cobrar duas vezes porque a tela não carregou.
Retry que ignora o orçamento do pedido
Três tentativas com timeout de 2s cada consomem 6 segundos mais as esperas — e o cliente que tinha 3 segundos de paciência já foi embora na primeira. Cada tentativa gasta do mesmo orçamento; quando ele acaba, não há tentativa que valha.
Repetir o que nunca vai melhorar
400,401,404e422devolvem exatamente o mesmo resultado na segunda tentativa. Repeti-los desperdiça recurso e, sob incidente, rouba capacidade de quem tinha chance de sucesso.Ignorar o Retry-After
Quando o servidor responde
429comRetry-After, ele está dizendo exatamente quando estará pronto. Sobrepor isso com o seu próprio backoff é repetir contra a vontade de quem sabe — e o caminho mais curto para ter a chave bloqueada.
Backoff exponencial com full jitter
// ERRADO: intervalo fixo. Mil clientes falham juntos e voltam juntos,
// em ondas sincronizadas que impedem o servico de se levantar.
for (let i = 0; i < 3; i++) {
try { return await chamar(); } catch { await dormir(1000); }
}
// CERTO: espera exponencial + jitter (ruido aleatorio que dessincroniza).
interface OpcoesRetry {
tentativas: number;
baseMs: number;
tetoMs: number;
/** injetado: sorteio nunca acontece dentro da funcao pura. */
aleatorio: () => number;
}
function atraso(tentativa: number, o: OpcoesRetry): number {
const exponencial = Math.min(o.tetoMs, o.baseMs * 2 ** tentativa);
// "full jitter": sorteia em [0, exponencial]. Espalha melhor que somar ruido.
return o.aleatorio() * exponencial;
}
async function comRetry<T>(fn: () => Promise<T>, o: OpcoesRetry): Promise<T> {
let ultimo: unknown;
for (let i = 0; i < o.tentativas; i++) {
try {
return await fn();
} catch (e) {
if (!valeTentarDeNovo(e)) throw e; // 400 nao melhora tentando
ultimo = e;
await dormir(atraso(i, o));
}
}
throw ultimo;
}import random
def atraso(tentativa: int, base_ms: int, teto_ms: int, aleatorio=random.random) -> float:
"""Full jitter: sorteia em [0, exponencial]."""
exponencial = min(teto_ms, base_ms * 2 ** tentativa)
return aleatorio() * exponencial / 1000
# So repete o que pode melhorar sozinho.
REPETIVEIS = {408, 429, 500, 502, 503, 504}
def vale_tentar(status: int) -> bool:
# 400, 401, 403, 404, 422: tentar de novo da o mesmo resultado
return status in REPETIVEIS
# 429 costuma vir com Retry-After. Respeitar e obrigatorio:
# o servidor esta dizendo exatamente quanto esperar.
def espera(resposta, tentativa, base_ms, teto_ms):
if (ra := resposta.headers.get("Retry-After")):
return float(ra)
return atraso(tentativa, base_ms, teto_ms)Quando usar × quando evitar
Use quando
3- Falhas de rede, timeouts,
502,503e429. - Operações idempotentes ou com chave de idempotência.
- Numa camada só, a mais próxima da dependência que falha.
Evite quando
3- Erros de cliente da família
4xxque não são408nem429. - Escritas sem garantia de idempotência.
- Quando o orçamento de tempo do pedido já se esgotou.
Testar o que ficou
6 armadilhas de Retry com backoff e jitter viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
O cabeçalho Retry-AfterO servidor diz explicitamente quanto esperar — ignorar isso é repetir contra a vontade de quem sabe.Redelivery de filaMensagem não confirmada volta para a fila e é reentregue: é retry embutido no protocolo.Retransmissão do TCPO protocolo já faz backoff exponencial quando um segmento não é confirmado a tempo.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Espera crescente com ruido, e so no que pode melhorar.
await dormir(aleatorio() * Math.min(teto, base * 2 ** tentativa));O que ele cobra
Nenhum salto a mais entre a chamada e o efeito.
- A latência do pior caso multiplica pelo número de tentativas
- A operação precisa ser idempotente, o que nem sempre é de graça
Não vale se: o erro é determinístico. Repetir um 400 devolve o mesmo 400, gastando recurso alheio.
Onde isto aparece
Continue por aqui
- TimeoutTodo pedido que sai da sua máquina precisa de um prazo máximo. Sem ele, a falha mais comum em produção não é o erro — é a espera infinita, que consome recursos até derrubar quem ainda estava saudável.
- Circuit BreakerQuando uma dependência já está claramente fora do ar, continuar chamando não ajuda ninguém: gasta o seu prazo e atrapalha a recuperação dela. O disjuntor detecta o padrão de falha e passa a recusar na hora, até que valha a pena tentar de novo.
- IdempotênciaExecutar a mesma operação duas vezes tem o mesmo efeito de executar uma: é o que torna o retry seguro. Em rede, onde 'não sei se foi' é resposta comum, é a diferença entre reenviar e cobrar em dobro.