Blog

Retentativa sem teto: quando o cliente insistente vira o próprio ataque

A dependência de pagamentos ficou fora do ar por doze minutos. Quando ela voltou, o serviço não voltou junto: passou as três horas seguintes recebendo nove vezes o tráfego normal, com a CPU no teto, a fila de conexões cheia e a taxa de erro alta o bastante para manter a situação exatamente como estava. O time de segurança olhou os gráficos e abriu um incidente de negação de serviço. Não havia atacante. Oitenta e cinco por cento daquele tráfego vinha de clientes legítimos repetindo pedidos que tinham falhado: uma versão do aplicativo que tentava de novo a cada segundo sem limite, a integração de um parceiro que reenviava o lote inteiro a cada falha parcial, uma fila de sincronização offline que esvaziou em todos os celulares no mesmo minuto em que a rede voltou. Este artigo é sobre o lado do servidor desse problema: por que a retentativa sem teto de clientes que você não controla transforma uma queda curta em uma longa, por que o tráfego resultante tem a assinatura de um ataque e não pode ser tratado como um, como enxergar a repetição do lado do servidor quando o cliente não avisa que está repetindo, qual contrato de resposta ensina o cliente a parar, como descartar tentativas antes de pedidos originais quando a capacidade acaba, e o que corrigir na origem quando o cliente insistente é o seu próprio aplicativo.

2026-09-23 / Arquitetura / 17 min

01

A queda termina e o tráfego não volta ao normal

O comportamento esperado depois de uma falha curta é uma recuperação curta: a dependência volta, as requisições voltam a ter sucesso e o tráfego retorna ao patamar anterior em alguns minutos. O que aconteceu nesse incidente foi diferente, e a diferença está na população de clientes. Durante os doze minutos de falha, cada cliente que recebeu erro não desistiu. Ele guardou o pedido e continuou tentando, e cada tentativa que falhou gerou outra. Quando a dependência voltou, o serviço não encontrou o tráfego normal esperando por ele: encontrou o tráfego normal somado a doze minutos de pedidos acumulados, todos chegando ao mesmo tempo.

Esse volume é suficiente para derrubar o serviço de novo, e aí está o mecanismo que prolonga o incidente. Com o serviço sobrecarregado, uma parte das requisições volta a falhar por tempo esgotado, e cada falha gera uma nova tentativa, que chega a um serviço ainda mais sobrecarregado. O sistema entra em um estado estável que não é o saudável: a carga que o mantém caído é produzida pela própria queda. Ele só sai desse estado quando alguém reduz a carga de fora, desligando uma rota, bloqueando um cliente ou esperando que os usuários desistam por conta própria.

Tráfego recebido pelo servico (1x = pico normal de uma terca-feira)

  9x |              ##
     |              ####
  6x |              ######   ###
     |              ######## #####  ###
  3x |              ###############  ######  ##
     |              #########################  ###
  1x |##########################################################
     +-----+--------+--------+--------+--------+--------+-------
         14:00    14:12    14:40    15:10    16:00    17:10
                 volta a    |        |        |        normal
               dependencia  +-- serviço recai por sobrecarga, repete
                                a cada onda de tentativas sincronizadas

  Queda da dependencia: 12 minutos
  Queda percebida pelo usuario: 3 horas e 10 minutos
  Participacao de repeticoes no pico: 85% das requisicoes recebidas

Visto de fora, o gráfico é idêntico ao de um ataque volumétrico, e a reação instintiva é tratá-lo como um: ativar a regra do firewall de aplicação, bloquear os endereços com mais requisições, ligar o desafio contra robôs. As três medidas pioram o problema. Os endereços com mais requisições são os clientes mais importantes, como a integração do maior parceiro, que envia muito porque vende muito. O desafio contra robôs quebra o aplicativo e as integrações, que não conseguem resolvê-lo, e transforma a falha temporária em falha permanente para quem estava apenas repetindo um pedido legítimo. O tráfego é abusivo no efeito e legítimo na origem, e a defesa precisa separar as duas coisas.

02

De onde vem a insistência sem teto

Quase nenhum cliente foi escrito para insistir para sempre. A insistência sem teto surge da combinação de decisões individualmente razoáveis, tomadas em camadas diferentes por pessoas diferentes, cada uma assumindo que as outras não repetem. Conhecer as fontes importa porque cada uma pede uma correção diferente, e várias delas estão fora do alcance do time que opera o servidor.

FonteComo a repetição acontecePor que não tem tetoQuem pode corrigir
Aplicativo em versão antigaLaço de repetição com intervalo fixo ao falharA versão com o defeito continua instalada por meses em aparelhos que não atualizamVocê, mas só para versões futuras
SDK sobre SDKO código do cliente repete e a biblioteca embaixo dele também repeteCada camada tem teto próprio, e os tetos se multiplicam em vez de se somarQuem integra, se souber que a biblioteca repete
Integração de parceiro em loteUma falha parcial faz o lote inteiro ser reenviadoO agendador roda de novo no próximo ciclo com o lote acumulado, que só cresceO parceiro, com a sua orientação
Fila de sincronização offlineO aparelho guarda pedidos enquanto está sem rede e envia tudo ao reconectarTodos os aparelhos reconectam juntos quando a falha terminaVocê, no aplicativo
Consumidor de fila com reentregaA mensagem que falhou volta para a fila e é processada de novoSem contador de entregas nem fila de mensagens mortas, a mesma mensagem volta indefinidamenteVocê, na configuração do consumidor
Repetição de erro determinísticoO cliente repete qualquer erro, inclusive requisição inválida ou não autorizadaO pedido nunca vai ter sucesso, então nenhuma condição encerra o laçoQuem escreveu o cliente, com um contrato de erro claro

A segunda linha da tabela é a que produz os números mais altos, porque o efeito é multiplicativo. Se o código do aplicativo faz até três tentativas, a biblioteca HTTP embaixo dele faz outras três para cada uma, o proxy de saída da rede corporativa repete duas vezes em caso de conexão reiniciada e a mensagem chegou de uma fila que reentrega até cinco vezes, uma única ação do usuário pode produzir noventa chamadas no servidor. Nenhum dos quatro números parece exagerado isoladamente, e cada time que escolheu o seu tinha uma boa razão.

Uma acao do usuario, quatro camadas que repetem sem saber umas das outras

  fila (ate 5 entregas)
    └─ codigo do app (ate 3 tentativas)
         └─ biblioteca HTTP (ate 3 tentativas)
              └─ proxy de saida (ate 2 tentativas)
                   └─ servidor

  Pior caso = 5 x 3 x 3 x 2 = 90 chamadas para 1 acao
  Durante a queda, com todas as tentativas falhando,
  o pior caso deixa de ser teorico e vira o caso comum.

A última linha é a mais barata de corrigir e a mais comum. Um cliente que repete um erro de validação vai repeti-lo para sempre, porque o mesmo corpo sempre produz o mesmo erro. Esse tráfego não aparece apenas durante incidentes: ele existe o tempo todo, em volume baixo, e passa despercebido porque o servidor responde rápido a requisições inválidas. Em uma auditoria típica, entre dois e cinco por cento do tráfego total de uma API pública é repetição de pedidos que nunca vão ter sucesso.

03

Enxergar a repetição do lado do servidor

O servidor não sabe, por padrão, que uma requisição é uma repetição. Para ele, a décima tentativa do mesmo pedido é idêntica a uma requisição nova, e o painel mostra apenas o total. Essa cegueira é a razão pela qual o incidente do início foi classificado como ataque: sem saber quanto do tráfego era repetição, não havia como distinguir clientes insistentes de agressores. O primeiro passo da defesa é tornar a repetição visível, e existem duas fontes de informação para isso.

  • O que o cliente declara: um cabeçalho com o número da tentativa, que o seu SDK pode enviar e que você pode pedir aos parceiros que enviem. É barato e preciso, mas só existe para clientes que você controla ou convenceu.
  • O que o servidor deduz: uma impressão digital da requisição, calculada a partir do cliente, da rota e da chave de idempotência ou do conteúdo do corpo, e comparada com as impressões recentes do mesmo cliente. Funciona para qualquer cliente, inclusive os que você não controla, com o custo de uma memória de curto prazo.
// Deteccao de repeticao do lado do servidor, sem depender do cliente avisar.
// A impressao digital identifica "o mesmo pedido" dentro de uma janela curta,
// e a razao de repeticao por cliente vira metrica e criterio de descarte.
import { createHash } from 'node:crypto';

const JANELA_MS = 60_000;
const impressoesRecentes = new Map(); // impressao -> { visto_em, vezes }

// O corpo cru precisa ser guardado pelo parser, por exemplo:
// app.use(express.json({ verify: (req, _res, buf) => { req.corpoBruto = buf; } }));
function impressaoDaRequisicao(req) {
  const chaveIdempotencia = req.get('idempotency-key');
  const partes = chaveIdempotencia
    ? [req.clienteId, chaveIdempotencia]
    : [
        req.clienteId,
        req.method,
        req.path,
        createHash('sha256').update(req.corpoBruto || '').digest('hex'),
      ];
  return createHash('sha256').update(partes.join('|')).digest('base64url');
}

export function detectarRepeticao(req, _res, next) {
  const agora = Date.now();
  const impressao = impressaoDaRequisicao(req);
  const anterior = impressoesRecentes.get(impressao);
  const declarada = Number(req.get('x-tentativa') || 0);

  const repetida = declarada > 0 || (anterior && agora - anterior.visto_em < JANELA_MS);
  const vezes = anterior ? anterior.vezes + 1 : 1;
  impressoesRecentes.set(impressao, { visto_em: agora, vezes });

  // Disponivel para o controle de admissao e para o log estruturado.
  req.repeticao = { repetida: Boolean(repetida), vezes, declarada };

  metricas.increment('requisicoes_total', {
    cliente: req.clienteId,
    repetida: String(Boolean(repetida)),
  });
  next();
}

// Limpeza periodica: sem ela o mapa cresce com o trafego e vira o proximo
// problema de memoria. unref() evita que o timer segure o processo aberto.
setInterval(() => {
  const limite = Date.now() - JANELA_MS;
  for (const [impressao, dado] of impressoesRecentes) {
    if (dado.visto_em < limite) impressoesRecentes.delete(impressao);
  }
}, JANELA_MS).unref();

Com a métrica rotulada por cliente e pelo indicador de repetição, a razão de repetição passa a ser o sinal mais útil do painel durante um incidente. Em operação normal ela fica abaixo de dois ou três por cento. Quando uma dependência falha, ela sobe, e o valor dela na recuperação responde a pergunta que o time de segurança não conseguiu responder: se oitenta por cento do tráfego é repetição de pedidos de clientes conhecidos, não é ataque, é demanda acumulada, e a resposta certa é organizar a fila e não bloquear a porta.

Duas ressalvas sobre a implementação. O mapa em memória funciona por instância, o que é suficiente para métrica e para descarte local, porque a razão de repetição de uma amostra é representativa do todo; um armazenamento compartilhado só se justifica se a decisão precisar ser exata entre instâncias. E a impressão pelo conteúdo do corpo pode marcar como repetidos dois pedidos legítimos idênticos, como duas consultas iguais em sequência, o que é aceitável para um sinal estatístico e é o motivo pelo qual a chave de idempotência, quando existe, tem precedência.

04

O contrato de resposta que ensina o cliente a parar

Um cliente só consegue parar de insistir se a resposta disser a ele que deve parar e por quanto tempo. A maioria das APIs responde a sobrecarga com um código genérico de erro de servidor, sem corpo e sem indicação de espera, e deixa para cada cliente a decisão sobre o que fazer. Com essa resposta, o comportamento de cada cliente depende inteiramente de quem o escreveu, e o pior deles define a carga que o servidor recebe. O contrato de resposta existe para reduzir essa variação.

SituaçãoCódigoO que a resposta deve dizerO que o cliente deve fazer
Limite do cliente excedido429Retry-After com o tempo até a cota se renovarEsperar pelo menos o tempo indicado; não repetir antes
Servidor sobrecarregado ou dependência fora503Retry-After com variação aleatória por clienteEsperar o tempo indicado, que já vem espalhado
Requisição inválida ou incompleta400 ou 422Campo repetivel igual a false e o motivo em formato legível por máquinaNunca repetir o mesmo corpo; corrigir ou descartar
Credencial inválida ou sem permissão401 ou 403Campo repetivel igual a falseNunca repetir; renovar a credencial uma vez e parar se falhar de novo
Conflito com estado atual409O estado atual ou onde consultá-loConsultar o estado antes de decidir; repetir cegamente não resolve
Tempo esgotado em operação não idempotente504Identificador para consultar o resultadoConsultar o resultado antes de repetir, para não duplicar o efeito

O detalhe mais importante da tabela está na segunda linha. Se o servidor responde a todos os clientes com Retry-After de trinta segundos, ele não evitou a onda, apenas marcou a hora dela: todos os clientes que obedecem voltam juntos daqui a trinta segundos. O tempo de espera precisa ser espalhado pelo próprio servidor, porque não dá para confiar que cada cliente vai adicionar variação aleatória por conta própria. E a variação precisa ser estável por cliente, para que o mesmo cliente não receba um valor diferente a cada tentativa e acabe sempre escolhendo o menor.

// Resposta de sobrecarga que espalha as tentativas no tempo em vez de
// marcar a hora da proxima onda. O deslocamento e deterministico por cliente:
// cada um recebe sempre a mesma fatia da janela.
import { createHash } from 'node:crypto';

const ESPERA_BASE_S = 10;
const JANELA_ESPALHAMENTO_S = 50;

function deslocamentoDoCliente(clienteId) {
  const hash = createHash('sha256').update(String(clienteId)).digest();
  return hash.readUInt32BE(0) % JANELA_ESPALHAMENTO_S;
}

export function responderSobrecarga(req, res, motivo) {
  const esperaS = ESPERA_BASE_S + deslocamentoDoCliente(req.clienteId);

  res.set('Retry-After', String(esperaS));
  return res.status(503).json({
    erro: motivo,
    repetivel: true,
    tentar_apos_s: esperaS,
    // Quantas vezes vimos este mesmo pedido: ajuda quem investiga do lado do
    // cliente a perceber que o laco de repeticao dele nao tem teto.
    tentativas_observadas: req.repeticao?.vezes ?? 1,
  });
}

export function responderErroDeterministico(res, status, codigo, detalhe) {
  // Sem Retry-After de proposito: a mesma requisicao vai falhar do mesmo jeito.
  return res.status(status).json({ erro: codigo, repetivel: false, detalhe });
}

O campo repetivel no corpo resolve um problema que o código de status sozinho não resolve. Muitas bibliotecas de cliente decidem se repetem pela faixa do código, repetindo tudo que é da família cinco e nada da família quatro, o que funciona mal nas bordas: um 409 às vezes merece nova tentativa depois de consultar o estado, e um 500 causado por um corpo que quebra o servidor nunca vai ter sucesso. Um campo explícito, documentado e testado no contrato da API, transfere a decisão para quem tem a informação, que é o servidor.

05

Descartar tentativas antes de pedidos originais quando a capacidade acaba

O contrato de resposta reduz a insistência dos clientes que obedecem. Os que não obedecem, como a versão antiga do aplicativo que ninguém consegue atualizar, continuam chegando, e o servidor precisa de uma forma de proteger a capacidade que resta para quem ainda não foi atendido. A ideia central é que, sob sobrecarga, nem toda requisição vale o mesmo: um pedido original de um usuário que acabou de chegar tem mais valor que a décima tentativa de um pedido que já falhou nove vezes, porque a chance de a décima tentativa virar um resultado útil é menor e o custo que ela impõe ao sistema é o mesmo.

O controle de admissão abaixo implementa essa prioridade com dois limiares de concorrência. Abaixo do primeiro, tudo entra. Entre o primeiro e o segundo, só entram pedidos originais, e as repetições recebem a resposta de sobrecarga com espera espalhada. Acima do segundo, nada entra. A rejeição acontece antes de qualquer trabalho caro, o que é essencial: rejeitar depois de consultar o banco não economiza nada.

// Controle de admissao que descarta repeticoes antes de pedidos originais.
// Usa a marcacao feita por detectarRepeticao e a resposta de responderSobrecarga.
import { responderSobrecarga } from './respostas.js';

const CAPACIDADE = 200;                                // requisicoes simultaneas sustentaveis
const LIMIAR_REPETICOES = Math.floor(CAPACIDADE * 0.7); // acima disso, repeticao nao entra

let emAndamento = 0;

export function controleDeAdmissao(req, res, next) {
  const repetida = req.repeticao?.repetida === true;

  if (emAndamento >= CAPACIDADE) {
    metricas.increment('admissao_rejeitada', { motivo: 'capacidade', repetida: String(repetida) });
    return responderSobrecarga(req, res, 'capacidade_esgotada');
  }

  if (repetida && emAndamento >= LIMIAR_REPETICOES) {
    // Os 30% finais da capacidade ficam reservados para pedidos novos.
    metricas.increment('admissao_rejeitada', { motivo: 'reserva', repetida: 'true' });
    return responderSobrecarga(req, res, 'repeticao_adiada');
  }

  emAndamento += 1;
  let liberado = false;
  const liberar = () => {
    // finish e close podem disparar os dois; o contador so pode cair uma vez.
    if (liberado) return;
    liberado = true;
    emAndamento -= 1;
  };
  res.on('finish', liberar);
  res.on('close', liberar);
  next();
}

// Ordem no pipeline: identificar cliente -> detectarRepeticao -> controleDeAdmissao
// -> rotas. A deteccao precisa vir antes para a admissao conseguir priorizar.

O efeito desse mecanismo sobre a recuperação é desproporcional ao tamanho dele. No incidente do início, o serviço gastava a maior parte da capacidade processando repetições que falhavam por tempo esgotado, o que produzia mais repetições. Com a reserva, a fatia de capacidade destinada a pedidos originais continua funcionando mesmo no pico, os usuários novos são atendidos, e as repetições são empurradas para frente no tempo de forma espalhada, drenando a demanda acumulada em vez de se somarem a ela.

Esse controle convive com o limite de taxa por cliente, mas resolve outro problema. O limite de taxa protege o serviço de um cliente que consome mais do que o combinado em condições normais. O controle de admissão por tipo de requisição protege o serviço dele mesmo, no momento em que a capacidade encolheu e a demanda acumulada chegou toda de uma vez. Um cliente perfeitamente dentro da sua cota pode, junto com outros mil clientes dentro das cotas deles, produzir a onda que derruba o serviço na volta.

06

Corrigir na origem quando o cliente insistente é o seu

Tudo o que foi descrito até aqui é defesa. Quando o cliente insistente é o seu próprio aplicativo ou o SDK que você distribui para parceiros, existe uma correção mais barata e mais definitiva: dar teto à insistência na origem. O teto tem quatro componentes, e faltar qualquer um deles reabre o problema por um caminho diferente.

// Cliente com teto de insistencia: numero maximo de tentativas, prazo total,
// espera com variacao aleatoria completa, respeito ao Retry-After e parada
// imediata em erro que o servidor declarou como nao repetivel.
const PADRAO = {
  tentativasMax: 4,        // incluindo a primeira
  prazoTotalMs: 20_000,    // depois disso desiste, mesmo com tentativas sobrando
  esperaBaseMs: 500,
  esperaMaxMs: 8_000,
};

const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function lerRepetivel(resposta) {
  if (resposta.status === 429 || resposta.status === 503) return true;
  if (resposta.status < 500) return false;
  try {
    const corpo = await resposta.clone().json();
    return corpo.repetivel !== false;
  } catch {
    return true; // 5xx sem corpo legivel: tratado como transitorio
  }
}

export async function chamarComTeto(url, opcoes = {}, config = PADRAO) {
  const inicio = Date.now();

  for (let tentativa = 0; tentativa < config.tentativasMax; tentativa += 1) {
    const resposta = await fetch(url, {
      ...opcoes,
      headers: { ...opcoes.headers, 'x-tentativa': String(tentativa) },
    });

    if (resposta.ok) return resposta;
    if (!(await lerRepetivel(resposta))) return resposta; // erro deterministico: para aqui

    // Retry-After do servidor tem precedencia sobre o calculo local.
    const retryAfterS = Number(resposta.headers.get('retry-after'));
    const exponencial = Math.min(config.esperaMaxMs, config.esperaBaseMs * 2 ** tentativa);
    const espera = Number.isFinite(retryAfterS) && retryAfterS > 0
      ? retryAfterS * 1000
      : Math.random() * exponencial; // variacao completa: espalha os clientes

    const ultima = tentativa === config.tentativasMax - 1;
    if (ultima || Date.now() - inicio + espera > config.prazoTotalMs) return resposta;

    await esperar(espera);
  }
}

O prazo total é o componente mais esquecido. Um número máximo de tentativas sem prazo total permite que um servidor que manda esperar sessenta segundos a cada resposta segure o cliente por quatro minutos, com o usuário olhando para uma tela carregando. E um prazo total sem número máximo permite dezenas de tentativas rápidas quando o servidor falha instantaneamente. Os dois limites juntos definem um envelope que o usuário e o servidor conseguem prever.

  1. Coloque o teto em uma única camada. Se o código do aplicativo repete, desligue a repetição da biblioteca HTTP embaixo dele, e documente no SDK distribuído a parceiros que ele já repete, para que o código deles não repita por cima.
  2. Dê à fila de sincronização offline um envio espalhado na reconexão: um atraso aleatório inicial de alguns segundos a alguns minutos, proporcional ao tamanho da fila, em vez de enviar tudo no instante em que a rede volta.
  3. Mantenha um interruptor remoto que reduz ou desliga as repetições do aplicativo por configuração, sem publicar versão nova, porque durante o incidente não há tempo para passar pela loja de aplicativos.
  4. Registre a versão do cliente em toda requisição e defina uma versão mínima suportada, com resposta clara para versões abaixo dela, para conseguir retirar de circulação a versão com o laço sem teto.
  5. Configure o consumidor de fila com número máximo de entregas e fila de mensagens mortas, e trate o aumento dela como alerta, não como depósito.
  6. Escreva um teste que simula o servidor respondendo 503 com Retry-After e verifica quantas chamadas o cliente fez e quanto tempo levou, porque sem teste o teto é removido na primeira refatoração.

O terceiro item é o que decide quanto dura o próximo incidente causado pelo aplicativo. Uma versão com defeito no laço de repetição leva semanas para sair dos aparelhos, e durante esse tempo a única alavanca disponível é o que o aplicativo lê do servidor. Um aplicativo que consulta, ao iniciar e periodicamente, um documento de configuração com o número máximo de tentativas e a espera mínima pode ser corrigido em minutos, mesmo em versões antigas, desde que a leitura dessa configuração tenha existido antes do defeito.

FAQ

Perguntas frequentes

Como diferenciar um cliente legítimo insistente de um ataque de verdade durante o incidente?

Pelo conteúdo do tráfego, não pelo volume. Um ataque volumétrico e uma onda de repetições legítimas têm o mesmo gráfico de requisições por segundo, mas diferem em quatro sinais que podem ser verificados em poucos minutos se a instrumentação existir antes do incidente. O primeiro é a razão de repetição: repetições legítimas são o mesmo pedido chegando várias vezes, com a mesma impressão digital ou a mesma chave de idempotência, enquanto tráfego de ataque costuma variar parâmetros para escapar de cache e de deduplicação. O segundo é a identidade: repetições legítimas vêm autenticadas, de clientes que já existiam antes do incidente e com a distribuição de clientes parecida com a de um dia normal, apenas multiplicada. O terceiro é a correlação temporal: a onda legítima começa no instante em que a dependência falhou ou voltou, e as ondas seguintes têm intervalo compatível com o laço de repetição de uma versão específica do cliente, o que aparece claramente quando o tráfego é agrupado pela versão declarada. O quarto é a rota: repetições se concentram nas rotas que falharam, enquanto ataques costumam mirar as mais caras ou a raiz. Quando os quatro sinais apontam para demanda acumulada, a resposta é o controle de admissão com prioridade para pedidos originais e a espera espalhada no Retry-After. Quando apontam para ataque, as ferramentas de proteção de borda são adequadas. O erro caro é aplicar a segunda resposta ao primeiro caso, porque desafios e bloqueios por endereço transformam a falha temporária do maior cliente em falha permanente.

O servidor pode confiar que o cliente vai respeitar o Retry-After?

Não, e o desenho da defesa deve partir do pressuposto de que uma parte relevante dos clientes vai ignorá-lo. O Retry-After é uma instrução, não uma imposição, e a obediência depende de o cliente ter sido escrito para lê-lo, o que muitas bibliotecas HTTP não fazem por padrão e muitas integrações feitas às pressas nunca implementam. Isso não torna o cabeçalho inútil: nos clientes que você controla e nos parceiros que você orienta, ele é a ferramenta mais eficaz para espalhar a demanda no tempo, e costuma cobrir a maior parte do volume. Para os clientes que ignoram, a camada seguinte é o controle de admissão, que rejeita as repetições deles antes de qualquer trabalho caro, a um custo por rejeição de microssegundos. A combinação é o que funciona: o cabeçalho reduz a quantidade de repetições que chega, e o controle de admissão garante que as que chegam mesmo assim não consumam a capacidade reservada aos pedidos novos. Um passo adicional, útil para parceiros, é registrar por cliente a taxa de requisições que chegam antes do tempo indicado no último Retry-After. Esse número, apresentado ao parceiro com datas e exemplos, costuma resolver em uma conversa um problema que meses de incidentes não resolveram, porque torna visível que o laço de repetição dele não tem teto.

Qual é o número certo de tentativas e a espera certa para um cliente?

O número certo é pequeno e a espera certa é definida pelo prazo que o usuário tolera, não pela vontade de conseguir a resposta a qualquer custo. Para chamadas interativas, em que um usuário espera diante da tela, três ou quatro tentativas no total, com espera exponencial e variação aleatória completa começando em algumas centenas de milissegundos e um prazo total entre dez e trinta segundos, cobrem as falhas transitórias que realmente se resolvem sozinhas, como uma conexão reiniciada ou uma instância reiniciando. Falhas que duram mais do que isso não são transitórias do ponto de vista do usuário, e continuar tentando só adia a mensagem de erro e adiciona carga ao servidor que está tentando se recuperar. Para trabalho em segundo plano, como sincronização ou envio de lotes, o número de tentativas pode ser maior e as esperas podem chegar a minutos, desde que exista um prazo final depois do qual o item vai para uma fila de revisão, e não volta ao início do laço. Em ambos os casos, três regras independem dos números: não repetir erros declarados como não repetíveis, obedecer ao Retry-After quando ele existir, e manter a repetição em uma única camada da pilha. Se for preciso um número para começar, quatro tentativas no total com vinte segundos de prazo para chamadas interativas é um ponto de partida defensável, a ser ajustado pela distribuição real de duração das falhas transitórias que o seu serviço observa.

Insistência sem teto é demanda acumulada, e demanda acumulada precisa de fila, não de porta fechada

A queda de doze minutos que vira indisponibilidade de três horas não é causada pelo servidor nem pela dependência: é causada pela soma de clientes legítimos que repetem sem teto, cada um com uma decisão razoável e nenhum com a visão do todo. O tráfego resultante parece ataque e precisa ser tratado como demanda. Tornar a repetição visível com impressão digital e cabeçalho de tentativa, responder com Retry-After espalhado por cliente e com um campo explícito de repetível, reservar capacidade para pedidos originais quando ela acaba, e dar teto à insistência na origem dos clientes que você controla transformam a recuperação em algo que acontece em minutos, sem depender de o último usuário desistir. Posso instrumentar a razão de repetição no seu serviço, desenhar o contrato de resposta e o controle de admissão para a sua capacidade real, e revisar o laço de repetição do seu aplicativo e do SDK que você distribui antes do próximo incidente.