Lógica de Retentativa API e Backoff Exponencial: Estratégias que Realmente Funcionam

Domine o exponential backoff com full jitter, cabeçalhos Retry-After, chaves de idempotência e circuit breakers, depois teste a lógica de retentativa da sua API com mocks do Apidog.

INEZA Felin-Michel

INEZA Felin-Michel

31 agosto 2026

Lógica de Retentativa API e Backoff Exponencial: Estratégias que Realmente Funcionam

Apidog para empresas

Implantação local

SSO & RBAC

Conforme SOC 2

Explorar Apidog Enterprise

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 o mais comumente mal executado. Um loop envolvendo uma chamada HTTP parece programação defensiva. Feito errado, ele transforma uma interrupção de 30 segundos em uma de 30 minutos, porque milhares de clientes sobrecarregam um servidor em dificuldades no mesmo momento. Feito certo, as novas tentativas absorvem falhas transitórias tão limpas que seus usuários nunca as percebem.

Este guia aborda a lógica de novas tentativas da qual os sistemas de produção dependem: quais códigos de status tentar novamente, a fórmula de backoff exponencial com jitter total, cabeçalhos Retry-After, chaves de idempotência, orçamentos de novas tentativas e disjuntores. Você também verá como provar que seu cliente se comporta corretamente simulando 429s e 503s com os servidores de mock do Apidog, porque um padrão de nova tentativa que você nunca testou contra um servidor com falha é um palpite, não um projeto. Equipes que constroem lógica de nova tentativa de API fintech aprendem isso da maneira mais cara; você não precisa.

Por que novas tentativas ingênuas pioram as interrupções

Imagine um serviço que lida com 1.000 requisições por segundo. Ele tem uma falha por cinco segundos. Cada cliente tenta novamente imediatamente, três vezes cada. Sua demanda de 1.000 rps se torna 4.000 rps direcionada a um servidor já de joelhos. Ele falha completamente. Agora, cada cliente tenta novamente.

Esse ciclo de feedback tem um nome: uma tempestade de novas tentativas (retry storm). A debandada sincronizada quando o servidor volta é o "thundering herd". O livro SRE do Google aponta esse padrão em seu capítulo sobre como lidar com falhas em cascata: novas tentativas sem backoff amplificam a carga exatamente quando o sistema menos pode suportar, e podem manter um serviço inoperante muito depois que a falha original for corrigida.

Dois erros de design causam a maioria das tempestades de novas tentativas:

A solução não é "nunca tentar novamente". A solução é tentar novamente seletivamente, com atrasos aleatórios crescentes e com um limite máximo rígido de quanto as suas novas tentativas adicionam de carga extra.

Tentar novamente essas falhas, nunca aquelas

Antes de qualquer cálculo de backoff, seu cliente precisa de uma tabela de decisão. Tentar novamente uma requisição que o servidor já rejeitou como inválida desperdiça capacidade e polui os logs. Tentar novamente uma falha transitória é o objetivo principal.

Tentar novamente estes:

Sinal Significado
429 Too Many Requests Você atingiu um limite de taxa. Diminua o ritmo e volte mais tarde.
502 Bad Gateway Um salto upstream retornou lixo. Frequentemente 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, timeouts de socket A requisição pode nunca ter chegado.

Um timeout de gateway 504 merece atenção especial: a origem pode ter processado sua requisição, mesmo que o gateway tenha desistido de esperar. Essa distinção importa quando chegamos à idempotência.

Nunca tentar novamente estes:

Sinal Significado
400 Bad Request Seu payload está malformado. Ele estará malformado na próxima vez também.
401 Unauthorized Suas credenciais estão erradas ou expiradas. Atualize o token, não entre em loop.
403 Forbidden Você não tem permissão. Tentar novamente não a concederá.
422 Unprocessable Entity A validação falhou. Corrija os dados, não o timing.

A regra: tentar novamente quando a falha é sobre o estado do servidor ou da rede. Falhar rapidamente quando a falha é sobre sua requisição. Um 429 fica no meio; ele pode ser tentado novamente, mas também é um sinal de que sua taxa geral de requisições precisa ser trabalhada, o que é um problema de limitação de taxa a ser resolvido antes de qualquer loop de nova tentativa.

A fórmula de backoff exponencial e por que o jitter importa

Backoff exponencial significa que cada nova tentativa espera mais tempo do que a anterior, dobrando por padrão:

delay = base * 2^retry_count

Com uma base de 500 ms, isso é 0,5s, 1s, 2s, 4s, 8s. Adicione um limite (digamos, 30 segundos) para que os atrasos não aumentem para minutos:

delay = min(cap, base * 2^retry_count)

Isso resolve o problema de "martelar" mas não o problema de sincronização. Se 5.000 clientes falharem no mesmo instante, o backoff exponencial simples faz com que todos os 5.000 voltem em t=0,5s, depois t=1s, depois t=2s. Ainda em ondas. Ainda uma debandada, apenas uma mais educada.

O jitter quebra a sincronização ao randomizar o atraso. O Blog de Arquitetura da AWS analisou os números em sua análise de backoff exponencial e jitter, simulando clientes competindo contra um recurso disputado. O backoff sem jitter ainda produzia picos agrupados de chamadas. O jitter total, que escolhe um atraso aleatório entre zero e o limite exponencial, produziu o menor número total de chamadas e tempos de conclusão próximos aos mais curtos:

delay = random_between(0, min(cap, base * 2^retry_count))

Esse resultado surpreende as pessoas. Aleatorizar até zero parece desleixado em comparação com um cronograma de duplicação organizado. Mas espalhar os clientes uniformemente pela janela é exatamente o que mantém a carga do servidor estável. A análise da AWS também testou "equal jitter" (metade fixo, metade aleatório) e "decorrelated jitter"; o full jitter e o decorrelated jitter se destacaram, e o full jitter é o mais simples de escrever corretamente. Use-o como seu padrão de nova tentativa, a menos que você tenha medições que digam o contrário.

Respeite o Retry-After quando o servidor disser

O backoff é o seu cliente adivinhando quanto tempo deve esperar. Às vezes, o servidor remove a adivinhação. O cabeçalho Retry-After, definido para respostas 429 e 503, carrega um número de segundos ou uma data HTTP:

HTTP/1.1 429 Too Many Requests
Retry-After: 12

Quando este cabeçalho está presente, ele substitui o seu backoff calculado. O servidor sabe quando sua janela de limite de taxa é redefinida ou quando sua manutenção termina; sua programação exponencial não. Clientes ignorando o Retry-After é uma das razões pelas quais os provedores escalam do throttling para proibições absolutas. Analise-o, respeite-o e ainda aplique seu limite e contagem máxima de novas tentativas para que um Retry-After: 86400 hostil ou com bug não possa travar seu worker por um dia.

Idempotência: a pré-condição para tentar novamente POST

Aqui está a armadilha naquele 504 anterior. GET, PUT e DELETE são idempotentes por contrato: enviá-los duas vezes deixa o sistema no mesmo estado. POST não é. Se um POST /v1/payments expirar depois que o servidor o processou, sua nova tentativa cria um segundo pagamento. Parabéns, você construiu uma máquina de cobrança dupla com excelente tempo de atividade.

A solução é uma chave de idempotência: um ID exclusivo gerado pelo cliente (geralmente um UUID) enviado como um cabeçalho em cada operação lógica. O servidor armazena a chave com a primeira resposta e reproduz essa resposta armazenada para qualquer duplicata. As requisições idempotentes do Stripe funcionam exatamente dessa forma, e a maioria das APIs de pagamento e provisionamento seguiram o exemplo.

Duas regras fazem as chaves funcionarem:

Se a API que você está chamando não suportar chaves de idempotência, não tente novamente gravações não idempotentes automaticamente. Sinalize a falha e deixe que um humano ou um trabalho de reconciliação decida.

Orçamentos de novas tentativas e disjuntores: a rota de fuga

O backoff define quando as novas tentativas acontecem. Ele não limita quantas acontecem. Durante uma interrupção longa, mesmo clientes com jitter bem ajustado acumulam carga de novas tentativas, e as novas tentativas em camadas se multiplicam: se o seu gateway de API tenta novamente 3 vezes e o seu cliente de serviço tenta novamente 3 vezes, um clique do usuário pode se tornar 9 requisições.

Dois mecanismos limitam o dano:

Orçamentos de novas tentativas. Em vez de "3 novas tentativas por requisição", force "as novas tentativas podem adicionar no máximo 10% de carga extra", medida em uma janela deslizante. Quando o orçamento é gasto, as falhas retornam imediatamente. Isso mantém a amplificação de novas tentativas limitada, não importa quantas requisições estejam falhando ao mesmo tempo. Linkerd e Envoy entregam isso como uma configuração de primeira classe.

Disjuntores. Rastreie a taxa de falhas por downstream. Quando ela cruza um limite, o disjuntor abre: as chamadas falham instantaneamente sem tocar na rede. Após um resfriamento, algumas requisições de teste verificam se a dependência se recuperou antes que o disjuntor feche novamente. Onde o backoff gentilmente desacelera a debandada, o disjuntor a cancela. Todo projeto sério de novas tentativas combina os dois, porque o backoff sozinho ainda envia todas as requisições eventualmente.

Um exemplo pronto para produção em Python

Aqui está o padrão completo em um só lugar: filtragem de status retentável, jitter total, suporte a Retry-After, uma chave de idempotência e um limite máximo de novas tentativas.

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)

Vale a pena notar: a chave é cunhada uma vez, fora do loop. Retry-After vence o backoff calculado, mas ainda respeita o limite. Status não retentáveis levantam uma exceção imediatamente. Se você está no lado JavaScript, a biblioteca axios-retry oferece a mesma forma com ganchos retryCondition e retryDelay; a tabela de decisão permanece idêntica.

Como testar o comportamento de novas tentativas antes que a produção o faça por você

A maioria das equipes envia código de nova tentativa que nunca executou seu branch de falha. O caminho feliz foi testado; o caminho 503 é executado pela primeira vez durante uma interrupção real. Você pode fazer melhor com dois recursos do Apidog.

Simule falhas com servidores de mock. O smart mock do Apidog permite que você defina um endpoint como /v1/payments e programe suas respostas. Faça-o retornar 503 para as duas primeiras chamadas e 200 na terceira, ou retornar um 429 com Retry-After: 5, ou adicione um atraso de 15 segundos para acionar o timeout do seu cliente. Aponte seu cliente para a URL do mock e observe o loop de nova tentativa lidar com cada cenário, sem necessidade de incidente de produção.

Afirme o comportamento do cliente com cenários de teste. Os cenários de teste do Apidog encadeiam requisições com asserções e verificações de tempo. Construa um cenário que seja disparado contra seu mock falho e afirme que a chamada eventualmente terá sucesso, o tempo total decorrido está dentro do seu envelope de backoff esperado e exatamente um recurso foi criado (provando que sua chave de idempotência fez seu trabalho). Conecte o cenário ao CI e sua lógica de nova tentativa será exercitada a cada commit, em vez de a cada interrupção.

Essa é a diferença entre "adicionamos novas tentativas" e "verificamos se nosso cliente sobrevive a uma dependência com limite de taxa e meio inoperante". Baixe o Apidog gratuitamente e você pode ter um servidor de mock com falha rodando contra seu cliente em cerca de dez minutos.

FAQ

Devo tentar novamente um 429?

Sim, e é o único status em que o servidor geralmente lhe diz como. Leia o cabeçalho Retry-After e espere pelo menos esse tempo; recorra ao backoff exponencial com jitter se o cabeçalho estiver ausente. Também trate 429s repetidos como um sinal para corrigir sua taxa de requisições com limitação do lado do cliente ou cache, não como operação normal.

O que é full jitter?

O full jitter escolhe cada atraso de nova tentativa uniformemente de forma aleatória entre zero e o limite exponencial: random(0, min(cap, base * 2^n)). Isso evita ondas de novas tentativas sincronizadas de muitos clientes. Nas simulações da AWS, ele superou o backoff simples e o equal jitter tanto no total de chamadas feitas quanto no tempo para conclusão, e é por isso que é o padrão nos SDKs da AWS.

É seguro tentar novamente requisições POST?

Somente quando a requisição é idempotente na prática, o que para POST significa enviar uma chave de idempotência que o servidor desduplica. Sem ela, uma nova tentativa após um timeout pode duplicar um pagamento, pedido ou registro, porque o servidor pode ter processado a requisição que você pensa que falhou. Agentes de IA que chamam APIs de escrita se deparam com isso constantemente; os padrões de recuperação de erro do agente são os mesmos abordados aqui: escritas com chave, novas tentativas limitadas e um disjuntor.

Quantas vezes devo tentar novamente?

Três a cinco tentativas lidam com quase todas as falhas transitórias; além disso, as taxas de sucesso se estabilizam enquanto a carga e a latência continuam a aumentar. Combine o limite por requisição com um orçamento global de novas tentativas (por exemplo, as novas tentativas podem adicionar 10% de tráfego extra) para que uma interrupção completa não multiplique sua carga. Se uma dependência permanecer inativa após sua última nova tentativa, isso é território de disjuntor, não de nova tentativa.

Pratique o design de API no Apidog

Descubra uma forma mais fácil de construir e usar APIs