This page is also available in English.

Publicado em 12 min de leitura

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.
Quatro ideias trabalham juntas:
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.
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.
Um cliente robusto costuma precisar dos dois:
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.
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:
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.
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.
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.
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:
temp = min(cap, base * 2^attempt)
sleep = temp // ainda sem jitter — não envie isso sozinhoSó 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.
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:
// 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.
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.
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.
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:
Não retente quando:
400 de validação, 401/403, payload inválido)Preferência da Amazon para control-plane e muitas operações de data-plane: retentar em um único ponto da pilha.
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.
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:
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.
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.
standard) a loops caseiros ao chamar APIs gerenciadas.4xx de validação/auth — trabalho inútil que nunca vai passarRetry-After / sinais de sobrecarga — brigar com backpressure em vez de cooperaradaptive pode atrasar a primeira chamada e não é default universalBackoff 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.
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.
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.
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.
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.
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.