Retry em APIs: backoff, jitter, idempotência e disjuntores
Sua chamada de API de pagamento falhou às 2h da manhã. Foi uma falha de rede, um limite de taxa ou um servidor inativo? A resposta decide se a nova tentativa salva a transação ou cobra duas vezes um cliente.
As novas tentativas (retries) são o padrão de resiliência mais comum em sistemas distribuídos — e também um dos mais mal executados. Um loop envolvendo uma chamada HTTP parece programação defensiva. Feito errado, transforma uma interrupção de 30 segundos em uma de 30 minutos, porque milhares de clientes sobrecarregam um servidor já em dificuldade. Feito certo, absorve falhas transitórias sem que os usuários percebam.
Este guia mostra a lógica usada em sistemas de produção: códigos de status retentáveis, backoff exponencial com full jitter, Retry-After, chaves de idempotência, orçamentos de retries e disjuntores. Também mostra como testar 429s e 503s com servidores de mock do Apidog. Um padrão de retry que nunca foi testado contra um servidor com falha é um palpite, não um projeto.
Por que novas tentativas ingênuas pioram as interrupções
Imagine um serviço que processa 1.000 requisições por segundo e fica indisponível por cinco segundos. Se cada cliente tentar novamente imediatamente, três vezes, a demanda passa para 4.000 RPS — justamente quando o servidor está mais vulnerável.
Esse ciclo de feedback é uma tempestade de novas tentativas (retry storm). Quando o servidor volta, a debandada sincronizada é o chamado thundering herd. O livro SRE do Google sobre como lidar com falhas em cascata explica que retries sem backoff amplificam a carga e podem prolongar a indisponibilidade muito depois da falha original.
Os dois erros mais comuns são:
- Nenhum atraso entre tentativas: as chamadas multiplicam a carga durante o pior momento.
- Atrasos fixos: se todos esperam exatamente um segundo, todos retornam juntos.
A solução não é nunca tentar novamente. É tentar seletivamente, com atrasos crescentes e aleatórios, além de um limite rígido para a carga extra.
Tente novamente estas falhas — e nunca aquelas
Antes de calcular o backoff, defina uma tabela de decisão. Repetir uma requisição que o servidor rejeitou como inválida desperdiça capacidade; repetir uma falha transitória é o objetivo do mecanismo.
Status e falhas retentáveis
| Sinal | Significado |
|---|---|
429 Too Many Requests |
Você atingiu um limite de taxa. Diminua o ritmo e tente mais tarde. |
502 Bad Gateway |
Um salto upstream retornou uma resposta inválida; costuma ser transitório. |
503 Service Unavailable |
O servidor está sobrecarregado ou reiniciando. |
504 Gateway Timeout |
Uma dependência upstream demorou demais. |
| Reinicializações de conexão, falhas de DNS e timeouts de socket | A requisição pode nunca ter chegado ao servidor. |
Um 504 merece atenção especial: a origem pode ter processado a requisição mesmo que o gateway tenha desistido de esperar. Essa distinção é importante para a idempotência. Veja também timeout de gateway 504.
Status que não devem ser repetidos automaticamente
| Sinal | Significado |
|---|---|
400 Bad Request |
O payload está malformado e continuará assim. |
401 Unauthorized |
As credenciais estão erradas ou expiradas. Atualize o token. |
403 Forbidden |
Você não tem permissão; repetir não a concederá. |
422 Unprocessable Entity |
A validação falhou; corrija os dados. |
A regra é simples:
Tente novamente quando a falha estiver relacionada ao estado do servidor ou à rede. Falhe rapidamente quando estiver relacionada à sua requisição.
Um 429 fica no meio: pode ser repetido, mas indica que sua taxa geral de requisições também precisa ser corrigida com limitação de taxa ou cache. Consulte como implementar limitação de taxa.
Backoff exponencial e a importância do jitter
O backoff exponencial aumenta o tempo de espera a cada tentativa:
delay = base * 2^retry_count
Com uma base de 500 ms, os atrasos são 0,5 s, 1 s, 2 s, 4 s e 8 s. Adicione um limite para evitar esperas excessivas:
delay = min(cap, base * 2^retry_count)
Isso evita martelar o servidor, mas não resolve a sincronização. Se 5.000 clientes falharem ao mesmo tempo, todos retornarão em t=0,5s, depois t=1s e depois t=2s. São ondas de tráfego, apenas mais espaçadas.
O jitter quebra essa sincronização ao randomizar o atraso. O estudo da AWS sobre backoff exponencial e jitter mostrou que o backoff sem jitter ainda produz picos agrupados, enquanto o full jitter reduz o número total de chamadas e mantém tempos de conclusão próximos dos menores:
delay = random_between(0, min(cap, base * 2^retry_count))
O full jitter escolhe aleatoriamente um atraso entre zero e o limite exponencial. Espalhar os clientes pela janela mantém a carga mais estável.
A AWS também analisou equal jitter — metade fixa e metade aleatória — e decorrelated jitter. O full jitter e o decorrelated jitter tiveram os melhores resultados; o full jitter é o mais simples de implementar corretamente. Use-o como padrão, salvo quando suas métricas indicarem outra estratégia.
Respeite Retry-After
O backoff é uma estimativa do cliente. O cabeçalho Retry-After, definido para respostas como 429 e 503, permite que o servidor informe quando tentar novamente. Ele pode conter segundos ou uma data HTTP:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Quando presente, Retry-After deve substituir o backoff calculado. O servidor conhece a janela de limitação ou o fim da manutenção; seu cliente não.
Ainda assim, aplique um limite máximo e uma contagem máxima de retries. Um valor malformado ou malicioso como Retry-After: 86400 não deve bloquear um worker por um dia. Consulte a documentação do cabeçalho Retry-After.
Idempotência: pré-condição para repetir um POST
Aqui está a armadilha do 504: GET, PUT e DELETE são idempotentes por contrato, mas POST não é. Se POST /v1/payments expirar depois de ser processado pelo servidor, uma nova tentativa pode criar um segundo pagamento.
A solução é uma chave de idempotência: um identificador único, normalmente um UUID, enviado em todas as tentativas de uma mesma operação lógica. O servidor armazena a chave e a primeira resposta, reproduzindo essa resposta para duplicatas. As requisições idempotentes do Stripe funcionam dessa forma, assim como muitas APIs de pagamento e provisionamento. Veja também a explicação sobre chaves de idempotência.
Duas regras são essenciais:
- Mesma operação, mesma chave: cada retry de um pagamento reutiliza a mesma chave; uma nova ação do usuário recebe outra.
- Gere a chave antes do primeiro envio: nunca a crie dentro do loop de retry.
Se a API não suportar idempotência, não repita automaticamente gravações não idempotentes. Registre a falha e encaminhe-a para uma pessoa ou para um processo de reconciliação.
Orçamentos de retries e disjuntores
O backoff define quando repetir, mas não quantas vezes. Durante uma interrupção prolongada, retries bem espaçados ainda acumulam carga. Além disso, retries em camadas se multiplicam: se o gateway repete três vezes e o cliente também repete três vezes, um clique pode gerar nove requisições.
Orçamento de novas tentativas
Em vez de permitir “três retries por requisição”, limite os retries a, por exemplo, 10% de carga extra em uma janela deslizante. Quando o orçamento acabar, retorne as falhas imediatamente.
Isso limita a amplificação independentemente do número de requisições em falha. Linkerd e Envoy oferecem esse recurso como configuração de primeira classe.
Disjuntores
Um disjuntor acompanha a taxa de falhas de cada dependência. Quando o limite é excedido, ele abre e as chamadas falham instantaneamente, sem acessar a rede. Depois de um período de resfriamento, algumas chamadas de teste verificam a recuperação antes de fechar o circuito novamente.
O backoff desacelera a debandada; o disjuntor a interrompe. Sistemas robustos usam os dois, porque o backoff sozinho ainda envia todas as requisições eventualmente.
Exemplo pronto para produção em Python
O exemplo combina:
- filtragem de status retentáveis;
- full jitter;
- suporte a
Retry-After; - chave de idempotência;
- limite máximo de retries.
import random
import time
import uuid
import requests
RETRYABLE = {429, 502, 503, 504}
BASE = 0.5 # seconds
CAP = 30.0 # ceiling on any single delay
MAX_RETRIES = 5
def create_payment(payload):
idempotency_key = str(uuid.uuid4()) # one key per logical payment
headers = {"Idempotency-Key": idempotency_key}
for retry_count in range(MAX_RETRIES + 1):
try:
resp = requests.post(
"https://api.acmepay.com/v1/payments",
json=payload,
headers=headers,
timeout=10,
)
if resp.status_code < 400:
return resp.json()
if resp.status_code not in RETRYABLE:
resp.raise_for_status() # 400/401/403/422: fail fast
retry_after = resp.headers.get("Retry-After")
except (requests.ConnectionError, requests.Timeout):
retry_after = None # network fault: fall through to backoff
if retry_count == MAX_RETRIES:
raise RuntimeError("payment failed after all retries")
if retry_after and retry_after.isdigit():
delay = min(CAP, float(retry_after))
else:
delay = random.uniform(
0,
min(CAP, BASE * 2 ** retry_count),
)
time.sleep(delay)
A chave é criada uma única vez, fora do loop. Retry-After substitui o backoff calculado, mas continua sujeito ao limite máximo. Status não retentáveis falham imediatamente.
No JavaScript, a biblioteca axios-retry oferece uma estrutura semelhante com os hooks retryCondition e retryDelay. A tabela de decisão continua a mesma.
Teste os retries antes de depender deles em produção
Muitas equipes testam apenas o caminho feliz. O branch de 503 é executado pela primeira vez durante uma interrupção real. É possível testar esse comportamento com dois recursos do Apidog.
Simule falhas com servidores de mock
O smart mock do Apidog permite configurar um endpoint como /v1/payments para:
- retornar
503nas duas primeiras chamadas e200na terceira; - retornar
429comRetry-After: 5; - atrasar a resposta por 15 segundos para acionar o timeout do cliente.
Aponte seu cliente para a URL do mock e observe como o loop reage, sem precisar esperar um incidente de produção.
Valide o comportamento com cenários de teste
Os cenários de teste do Apidog encadeiam requisições, asserções e verificações de tempo. Crie um cenário que:
- execute o cliente contra o mock com falha;
- confirme que a chamada eventualmente terá sucesso;
- verifique que o tempo total está dentro do envelope de backoff esperado;
- confirme que exatamente um recurso foi criado, provando que a chave de idempotência funcionou.
Conecte o cenário ao CI para exercitar a lógica a cada commit, e não apenas durante uma interrupção. Baixe o Apidog gratuitamente e configure um servidor de mock com falha em poucos minutos.
Essa é a diferença entre “adicionamos retries” e “verificamos que nosso cliente sobrevive a uma dependência limitada e parcialmente indisponível”.
FAQ
Devo tentar novamente um 429?
Sim. É o status em que o servidor normalmente informa como proceder. Leia Retry-After e espere pelo menos o tempo indicado. Se o cabeçalho estiver ausente, use backoff exponencial com jitter.
429s repetidos também indicam que sua taxa de requisições precisa ser corrigida com limitação do lado do cliente ou cache.
O que é full jitter?
O full jitter escolhe cada atraso aleatoriamente entre zero e o limite exponencial:
random(0, min(cap, base * 2^n))
Isso evita ondas sincronizadas de retries. Nas simulações da AWS, ele superou o backoff simples e o equal jitter tanto no total de chamadas quanto no tempo de conclusão. Por isso, tornou-se um padrão comum nos SDKs da AWS.
É seguro repetir requisições POST?
Somente quando a requisição é idempotente na prática. Para POST, isso normalmente significa enviar uma chave de idempotência que o servidor usa para eliminar duplicatas.
Sem essa proteção, um timeout pode duplicar um pagamento, pedido ou registro, pois o servidor pode ter processado a requisição original. Agentes de IA que chamam APIs de escrita enfrentam o mesmo problema; os padrões são escritas com chave, retries limitados e disjuntor. Consulte também padrões de recuperação de erro de agentes.
Quantas vezes devo tentar novamente?
Três a cinco tentativas cobrem a maioria das falhas transitórias. Depois disso, a taxa de sucesso tende a estabilizar, enquanto carga e latência continuam aumentando.
Combine um limite por requisição com um orçamento global — por exemplo, retries adicionando no máximo 10% de tráfego extra. Se a dependência continuar indisponível após a última tentativa, use um disjuntor em vez de continuar repetindo.
Referências
- Lógica de retry de API fintech
- Como lidar com falhas em cascata — Google SRE
- Timeout de gateway 504
- Como implementar limitação de taxa
- Análise de backoff exponencial e jitter — AWS
- Cabeçalho
Retry-After— MDN - Chave de idempotência
- Requisições idempotentes do Stripe
- Baixe o Apidog
- Padrões de recuperação de erro de agentes
Top comments (0)