This page is also available in English.

Voltar aos artigos

Timeouts, retries, jitter e estratégias de backoff

Publicado em 12 min de leitura

  • Distributed Systems
  • Resilience
  • HTTP
  • Reliability
Close-up de cabos de rede pretos e azuis conectados a hardware

Foto de Patrick Campanale no Unsplash

Uma chamada remota que nunca termina é pior do que uma que falha. Ela segura threads, sockets e memória enquanto o resto do sistema espera. Retries podem esconder um blip curto — ou multiplicar carga até a dependência cair. Na Amazon, timeouts, retries e backoff com jitter formam um kit para sobreviver a falhas parciais e transitórias sem transformar uma taxa pequena de erro em outage.

Este artigo é uma referência prática desse kit: o que cada peça faz, como elas interagem, quando usá-las e os erros que transformam “resiliência” em thundering herd.

O que são esses padrões

Quatro ideias trabalham juntas:

  • Timeout — limita quanto tempo você espera por uma chamada remota para liberar recursos
  • Retry — reenvia a mesma requisição lógica após uma falha transitória
  • Backoff — espera progressivamente mais entre tentativas para não martelar uma dependência em dificuldade
  • Jitter — adiciona aleatoriedade para que muitos clientes não retentem em sincronia

A Builders’ Library da Amazon deixa o problema claro: sistemas raramente falham como um bloco único. Sofrem falhas parciais (parte das requisições passa) e transitórias (o problema dura pouco). Timeouts limitam a espera. Retries compram outra chance de sucesso. Backoff e jitter impedem que essa segunda chance vire uma enchente sincronizada.

Como funcionam os timeouts

Timeout é o tempo máximo que um cliente espera a conclusão de uma requisição. Sem ele, uma dependência lenta ou travada pode esgotar memória, threads, conexões ou portas efêmeras no chamador.

Connection timeout vs request timeout

Um cliente robusto costuma precisar dos dois:

  • Connection timeout — quanto esperar para estabelecer a conexão TCP (e muitas vezes TLS)
  • Request / read timeout — quanto esperar pela resposta depois que a conexão está aberta

Prefira timeouts embutidos em clientes HTTP/RPC bem testados. Opções de socket de baixo nível, como SO_RCVTIMEO no Linux, são fáceis de aplicar mal: podem não cobrir DNS, handshake TLS ou a espera ponta a ponta do jeito que um deadline de cliente de alto nível cobre.

Como escolher o valor

A orientação da Amazon para chamadas dentro de uma região é baseada em métricas: escolha uma taxa aceitável de falso timeout (por exemplo 0,1%) e defina o timeout perto do percentil de latência correspondente da dependência (para 0,1%, algo perto de p99.9). Assim, a maioria das chamadas saudáveis cabe na janela, e esperas patológicas são cortadas.

Armadilhas que eles destacam:

  • Clientes na internet — some latência de rede realista; clientes podem estar no mundo todo
  • Distribuições de latência apertadas — quando p99.9 está perto de p50, acrescente margem para um pequeno aumento de latência não estourar quase tudo
  • Conexões frias — um timeout que inclui setup TLS pode disparar em falso após deploys, quando hosts novos estabelecem conexões; connection warm-up ou timeout um pouco maior no primeiro connect ajuda

Timeout alto demais desperdiça recursos. Timeout baixo demais cria falhas artificiais, dispara retries e pode transformar um pico leve de latência em outage autoinfligido.

Como funcionam retries e backoff

Retry assume que a próxima tentativa pode funcionar. Isso costuma ser verdade para blips de rede, sobrecarga breve em uma réplica ou um load balancer que fechou uma conexão ociosa. É falso para erros de validação, falhas de autenticação e muitos “not found” — nesses casos, falhe rápido.

Retries são egoístas

No ensaio da Builders’ Library, Marc Brooker é direto: um retry gasta mais tempo do servidor para melhorar a sua chance de sucesso. Quando falhas são raras, o trade-off aumenta a disponibilidade do cliente. Quando a dependência já está sobrecarregada, retries aumentam carga e podem atrasar a recuperação.

Backoff exponencial (com teto)

Em vez de retentar na hora, espere entre tentativas. O backoff exponencial multiplica a espera após cada falha, em geral com um teto (cap) para o atraso não crescer sem limite:

Text
temp = min(cap, base * 2^attempt)
sleep = temp   // ainda sem jitter — não envie isso sozinho

Só o teto cria outro modo de falha: quando todo mundo atinge o cap, todos retentam na mesma taxa constante. Limite o número de tentativas (e deadlines gerais) para falhar cedo na pilha de chamadas, em vez de retentar para sempre.

Full jitter, equal jitter e decorrelated jitter

Backoff sem aleatoriedade ainda agrupa. Se todo cliente espera exatamente 100 ms, depois 200 ms, depois 400 ms, você trocou um pico por uma série de picos sincronizados — o clássico thundering herd.

O post de 2015 no AWS Architecture Blog, de Marc Brooker, comparou fórmulas com um simulador de contenção. As variantes importantes:

TypeScript
// Full jitter (bom default)
sleep = random(0, min(cap, base * 2 ** attempt));

// Equal jitter (mantém uma espera mínima)
const temp = min(cap, base * 2 ** attempt);
sleep = temp / 2 + random(0, temp / 2);

// Decorrelated jitter (depende do sleep anterior)
sleep = min(cap, random(base, prevSleep * 3));

Naquela simulação, backoff exponencial sem jitter fez mais trabalho e levou mais tempo. Equal jitter foi pior que full jitter em trabalho e em tempo de conclusão. Full jitter e decorrelated jitter foram fortes; full jitter tendia a fazer menos trabalho total, decorrelated terminava um pouco mais rápido ao custo de mais trabalho. Para a maioria dos clientes, full jitter é o default certo: simples e gentil com o servidor.

Os AWS SDKs no modo de retry standard usam backoff exponencial com full jitter. Também usam bases diferentes por classe de erro — cerca de 50 ms para erros transitórios e 1.000 ms para throttling — com teto individual de 20 segundos por atraso.

Orçamentos e cotas de retry

Limites por requisição não bastam em um outage amplo. O livro de SRE do Google recomenda um retry budget por processo (por exemplo, só 60 retries por minuto; esgotado o orçamento, falhe sem retentar). Os AWS SDKs implementam ideia parecida como retry quota (token bucket): retries consomem tokens; sucessos reabastecem; com o orçamento vazio, o SDK falha rápido em vez de esperar retries sem chance.

A combinação — poucas tentativas por chamada mais orçamento compartilhado entre chamadas — é o que impede retries “úteis” de virarem falha em cascata.

Por que isso importa

Sem timeouts, uma dependência lenta pode prender grande parte da sua concorrência. Sem retries cuidadosos, 5% de erro pode virar muito mais tráfego do que a dependência aguenta. O capítulo de cascading failures do Google SRE conta a história da multiplicação: se o banco está sobrecarregado e frontend, backend e cliente retentam três vezes cada (quatro tentativas), uma ação do usuário pode gerar 4³ = 64 tentativas no banco.

Política correta de timeout + retry + jitter não é acabamento. É controle de carga sob estresse.

Quando usar (e quando não)

Use timeout em toda chamada remota — inclusive entre processos na mesma máquina. Prefira bibliotecas com deadlines explícitos que cubram a operação inteira.

Retente quando:

  • A falha parece transitória (timeouts, connection reset, HTTP 502/503/504, throttling com orientação de tentar depois)
  • A operação é idempotente, ou a API oferece idempotency key / client token
  • Ainda há orçamento de tentativas e retry budget
  • Você retenta em uma camada bem escolhida, não em cada hop

Não retente quando:

  • O erro é permanente para esta requisição (400 de validação, 401/403, payload inválido)
  • Um write pode já ter sido aplicado e a API não é idempotente (cobrança duplicada, create em dobro)
  • A dependência sinaliza sobrecarga sustentada e o retry budget acabou — falhe rápido e faça load shedding a montante
  • Várias camadas já retentam a mesma chamada lógica

Preferência da Amazon para control-plane e muitas operações de data-plane: retentar em um único ponto da pilha.

Vantagens e trade-offs

  • Retries agressivos, sem backoff — esconde blips curtos rápido, mas amplifica sobrecarga e cria thundering herds
  • Backoff exponencial, sem jitter — reduz a taxa média de retry, porém picos correlacionados continuam
  • Full jitter + backoff com teto — espalha carga e reduziu trabalho no servidor nas simulações do Brooker, com um pouco mais de variância de latência
  • Cotas / budgets de retry — protege a recuperação durante outages; alguns clientes veem erro mais cedo (em geral desejável)
  • Circuit breakers — isolamento duro de dependência ruim, mas o comportamento modal é difícil de testar e pode atrasar recuperação se mal calibrado

A Builders’ Library observa que circuit breakers são populares, mas introduzem comportamento modal difícil de testar. Limitar retries com token bucket costuma comprar boa parte da proteção com comportamento local mais simples — e esse padrão entrou no AWS SDK em 2016.

Exemplo prático

Um helper pequeno em TypeScript com backoff exponencial com teto e full jitter, classificação de falhas retentáveis e parada após um orçamento de tentativas:

TypeScript
type RetryOptions = {
  maxAttempts?: number; // inclui a primeira tentativa
  baseMs?: number;
  capMs?: number;
  isRetryable?: (error: unknown) => boolean;
};

function fullJitterDelay(attempt: number, baseMs: number, capMs: number) {
  const ceiling = Math.min(capMs, baseMs * 2 ** attempt);
  return Math.floor(Math.random() * (ceiling + 1));
}

async function withRetry<T>(
  operation: (signal: AbortSignal) => Promise<T>,
  {
    maxAttempts = 3,
    baseMs = 50,
    capMs = 20_000,
    isRetryable = () => true,
  }: RetryOptions = {},
): Promise<T> {
  let lastError: unknown;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 2_000);

    try {
      return await operation(controller.signal);
    } catch (error) {
      lastError = error;
      const isLast = attempt === maxAttempts - 1;
      if (isLast || !isRetryable(error)) throw error;

      const delayMs = fullJitterDelay(attempt, baseMs, capMs);
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    } finally {
      clearTimeout(timeout);
    }
  }

  throw lastError;
}

// Exemplo: retentar só falhas HTTP provavelmente transitórias
await withRetry(
  (signal) => fetch("https://api.example.com/items", { signal }).then(async (res) => {
    if (res.status === 429 || res.status >= 500) {
      throw Object.assign(new Error(res.statusText), { status: res.status });
    }
    if (!res.ok) {
      throw Object.assign(new Error(res.statusText), { status: res.status, fatal: true });
    }
    return res.json();
  }),
  {
    isRetryable: (error) =>
      !(error && typeof error === "object" && "fatal" in error && error.fatal),
  },
);

Em produção, também respeite Retry-After / atrasos dirigidos pelo servidor quando existirem, compartilhe um retry budget por processo e trate writes como inseguros até a API ser idempotente.

Idempotência torna o retry seguro

Timeout não significa que o servidor não fez nada. A resposta pode ter se perdido depois de um write bem-sucedido. O padrão preferido da Amazon é um identificador de requisição fornecido pelo chamador (por exemplo o ClientToken do EC2): o mesmo token em um retry devolve um resultado semanticamente equivalente, em vez de criar um segundo recurso. Projete APIs para idempotência antes de ligar retries agressivos no cliente.

Boas práticas

  1. Timeout em toda chamada remota — connection e request; prefira deadlines que cubram DNS/TLS quando fizer sentido.
  2. Derive timeouts da latência da dependência — orçamento de falso timeout primeiro, depois o percentil correspondente; some margem para internet e conexões frias.
  3. Retente só erros transitórios classificados — separe throttling de outras falhas quando puder (base maior para throttle).
  4. Use backoff exponencial com teto e full jitter como default.
  5. Limite tentativas e use retry budget para outages falharem rápido em vez de virar tempestade de retry.
  6. Retente em uma camada da pilha para evitar amplificação multiplicativa.
  7. Torne APIs com efeito colateral idempotentes (client tokens / idempotency keys) antes de habilitar retries.
  8. Coloque jitter também em trabalho periódico — storms alinhadas a cron são reais; a Amazon relata que espalhar jobs de minuto/hora reduziu a capacidade necessária para o mesmo trabalho.
  9. Prefira modos de retry de SDKs comprovados (por exemplo AWS SDK standard) a loops caseiros ao chamar APIs gerenciadas.
  10. Exercite caminhos de retry em testes e em produção — retry não testado é como se descobrem storms durante incidentes.

Erros comuns

  • Sem timeout — chamadas penduradas consomem a frota em silêncio
  • Timeout agressivo demais — blip de latência → timeout em massa → retry em massa → outage
  • Retentar 4xx de validação/auth — trabalho inútil que nunca vai passar
  • Retentar POSTs não idempotentes — efeitos colaterais duplicados
  • Retries em cada hop de microsserviço — 3×3×3 vira multiplicador de tráfego
  • Backoff exponencial sem jitter — rebanhos sincronizados
  • Retries ilimitados — threads presas em trabalho sem esperança durante blackout
  • Ignorar Retry-After / sinais de sobrecarga — brigar com backpressure em vez de cooperar

Alternativas e comparações

  • Fail fast, sem retry — melhor para writes não idempotentes e erros permanentes; suba o erro para uma política mais alta
  • Retry imediato (1×) — para flakes de conexão muito curtos; ainda assim use timeout e evite sob sobrecarga
  • Backoff exponencial com full jitter — default para clientes RPC/HTTP em geral, alinhado às comparações do Brooker
  • Adaptive / rate limiting no cliente — para workloads de um recurso com muito throttle; o AWS SDK adaptive pode atrasar a primeira chamada e não é default universal
  • Hedging (requisições especulativas em paralelo) — mira tail latency em reads; outra ferramenta que também pode amplificar carga
  • Circuit breaker — isolamento duro de dependência ruim; complementa budgets, e você deve testar os modos open/half-open

Perguntas frequentes

Qual a diferença entre backoff e jitter?

Backoff decide quão grande a janela de espera cresce após falhas (muitas vezes de forma exponencial). Jitter randomiza onde dentro dessa janela você dorme, para os clientes não se alinharem. Em geral você quer os dois.

Full jitter é sempre melhor que decorrelated jitter?

Nem sempre. Nas simulações do Brooker, full jitter usou menos trabalho, enquanto decorrelated completou um pouco mais rápido com mais trabalho. Para clientes típicos que protegem um backend compartilhado, full jitter é o default mais seguro. Prefira dados dos seus próprios testes de carga quando latência até a conclusão for o critério dominante.

Quantos retries eu deveria configurar?

Comece pequeno. O modo standard do AWS SDK usa por padrão 3 max attempts (uma tentativa mais duas retries) na maioria dos serviços. O Google SRE aconselha limitar retries por requisição e adicionar um budget por processo. Mais retries só ajudam se as falhas forem curtas e a dependência tiver capacidade sobrando.

Clientes mobile ou browser devem retentar como servidores?

Seja mais conservador. Milhões de clientes com ciclo lento de atualização podem prolongar uma política ruim de retry. Prefira retries no servidor, perto da dependência que falha, códigos de erro claros e menos tentativas no cliente.

Timeouts substituem retries?

Não. Timeouts impedem esperas sem limite; retries recuperam falhas transitórias depois de um timeout ou falha. Timeout sem política de retry falha rápido. Retry sem timeout pode empilhar tentativas concorrentes. Use os dois com backoff, jitter e budgets.

Conclusão

Trate toda chamada remota como potencialmente lenta, parcial ou mentirosa sobre se terminou. Coloque um timeout de verdade. Retente só quando a falha for transitória e a operação for segura. Espace tentativas com backoff exponencial com teto, quebre a sincronização com full jitter e proteja a frota com limites de tentativa mais um retry budget. Retente em um lugar, não em todos.

Se mudar só uma coisa no cliente esta semana: adicione full jitter ao backoff — e pare de retentar em três camadas do mesmo caminho de chamada.

Referências

  1. Timeouts, retries, and backoff with jitter — Amazon Builders’ Library, Marc Brooker (2019)
  2. Exponential Backoff And Jitter — AWS Architecture Blog, Marc Brooker (2015; atualizado em 2023)
  3. Making retries safe with idempotent APIs — Amazon Builders’ Library
  4. Retry behavior — AWS SDKs and Tools Reference Guide
  5. Announcing updated retry behavior for AWS SDKs and Tools — AWS Developer Tools Blog
  6. REL05-BP03 Control and limit retry calls — AWS Well-Architected Framework
  7. Addressing Cascading Failures — Google SRE Book
  8. What is Backoff For? — Marc Brooker (2022)

Comentários