Blog

Limite de taxa por cliente na borda: proteger o serviço sem punir o parceiro certo

O limite global entrou em produção numa sexta-feira à tarde para conter um parceiro que disparava sessenta chamadas por segundo, e às dezessete horas o time comemorou porque a latência voltou ao normal. Na segunda-feira o maior cliente da empresa abriu um chamado dizendo que a integração de estoque parou de funcionar às nove e quarenta, exatamente quando o parceiro abusivo também estava ativo. O limite funcionou: ele derrubou os dois. Este artigo mostra por que o limite global é um mecanismo de dano colateral e não de proteção, qual é a diferença prática entre os quatro algoritmos de limitação e por que a janela fixa produz o dobro do pico contratado, por que a identidade do chamador precisa ser resolvida antes de qualquer contagem e o que acontece quando ela é o endereço de rede, como o estado distribuído do contador sobrevive a várias instâncias de borda sem virar um gargalo, por que rejeitar não é a única resposta possível e quando enfileirar é melhor, quais cabeçalhos transformam a rejeição em contrato em vez de acidente, e quais quatro indicadores dizem se o limite está protegendo o serviço ou apenas empurrando o problema.

2026-09-16 / Arquitetura / 18 min

01

O limite global protege a média e sacrifica o cliente certo

O primeiro limite que qualquer serviço ganha costuma ser global: um teto de requisições por segundo aplicado na borda, sem distinguir quem chamou. Ele é fácil de configurar, aparece pronto em qualquer proxy reverso e tem a propriedade que todo mundo quer no momento do incidente, que é conter a carga imediatamente. O problema é que a contenção não escolhe alvo. Quando o teto é atingido, a rejeição recai sobre quem estava chegando naquele instante, e a probabilidade de alguém ser rejeitado é proporcional ao volume que ele envia. O cliente que manda três chamadas por minuto quase nunca é rejeitado. O cliente que manda duzentas por minuto porque o negócio dele exige isso é rejeitado o tempo todo, junto com o abusivo.

Existe um efeito mais perverso que aparece depois. O limite global cria uma competição entre clientes que nunca se conheceram. A capacidade que sobra para o cliente A depende do comportamento do cliente B, o que significa que a qualidade de serviço percebida por um integrador passa a ser função do que outro integrador fez na mesma janela. Isso é impossível de documentar num contrato, impossível de reproduzir em teste e impossível de explicar no chamado. A pergunta que o cliente faz, com razão, é qual é o limite dele, e sob teto global a resposta honesta é que depende dos outros.

A correção conceitual é tratar o limite como uma alocação de capacidade e não como um freio de emergência. Cada chamador recebe um orçamento próprio, verificável e independente, e o serviço passa a ter uma soma de orçamentos que ele sabe sustentar. Um chamador que ultrapassa o próprio orçamento consome apenas a fatia dele, e o excesso dele nunca chega perto da fatia de ninguém. É a mesma mudança de mentalidade que separa um pool de conexões compartilhado de pools isolados por carga: o isolamento custa um pouco de eficiência média e compra previsibilidade, que é o que um contrato de integração precisa vender.

ModeloQuem é penalizado no picoPrevisibilidade para o clienteQuando ainda faz sentido
Teto global na bordaQuem chegou no instante errado, proporcional ao volume legítimoNenhuma: o limite efetivo depende dos outros chamadoresVálvula de último recurso acima dos limites por cliente
Limite por endereço de redeTodos que compartilham saída NAT ou o mesmo provedor de nuvemBaixa: o mesmo cliente muda de identidade entre chamadasTráfego anônimo, antes da autenticação
Limite por credencial de clienteApenas o chamador que ultrapassou o próprio orçamentoAlta: o número entra no contrato e é reproduzívelPadrão para qualquer API com chamador identificado
Limite por credencial e por rotaO chamador, apenas na operação cara que ele abusouAlta, com granularidade que reflete o custo realQuando uma rota custa ordens de grandeza mais que as outras
Cota por custo estimadoO chamador, proporcional ao trabalho que gerouMédia: exige explicar a unidade de custo ao integradorCargas heterogêneas, como busca, relatório e exportação

A segunda linha da tabela merece um comentário porque ela é a escolha mais comum e a que mais produz chamado. Limitar por endereço de rede parece razoável até o dia em que um cliente corporativo inteiro sai por um único endereço de saída e consome o orçamento de mil funcionários como se fosse um chamador só, ou até o dia em que um integrador roda em funções serverless e aparece com um endereço novo a cada minuto, escapando de qualquer contagem. Endereço de rede é uma identidade útil antes da autenticação e enganosa depois dela.

02

A identidade do chamador é a decisão que antecede o algoritmo

Antes de escolher entre janela deslizante e balde de fichas, é preciso responder uma pergunta mais básica: o que exatamente está sendo contado. A chave de limitação é a unidade sobre a qual o orçamento se aplica, e escolher errado torna o algoritmo irrelevante, porque contar perfeitamente a coisa errada continua sendo errado. Em uma API com chamadores identificados, a chave quase sempre é derivada da credencial apresentada, e não do transporte.

A extração dessa chave tem uma ordem que importa. Ela precisa acontecer depois de validar a credencial, porque uma chave extraída de um token não verificado é uma chave que o atacante escolhe, e um atacante que escolhe a própria chave de limitação simplesmente gera uma chave nova a cada requisição e nunca é limitado. Ao mesmo tempo, a validação completa costuma envolver uma consulta, o que significa que o caminho de requisição não autenticada precisa ter o seu próprio limite, mais apertado e baseado em endereço de rede, para que o custo de validar credencial inválida não se torne o próprio vetor de ataque.

// Resolucao da chave de limitacao antes de qualquer contagem.
// A ordem importa: identidade nao confiavel nunca vira chave de orcamento.

const CLASSE_ANONIMA = 'anon';

/**
 * Deriva a chave de limitacao a partir do contexto da requisicao.
 * Retorna tambem a classe, porque orcamento e politica de excedente
 * mudam conforme o plano do cliente.
 */
export function resolverChaveDeLimite(req, credencialVerificada) {
  // 1) Sem credencial verificada, a unica identidade disponivel e a de rede.
  //    O orcamento aqui e pequeno de proposito: ele cobre login, troca de
  //    token e rotas publicas, nao trafego de integracao.
  if (!credencialVerificada) {
    return {
      chave: `${CLASSE_ANONIMA}:${enderecoDeOrigem(req)}`,
      classe: CLASSE_ANONIMA,
      escopo: 'rede',
    };
  }

  // 2) Com credencial verificada, a identidade estavel e o cliente,
  //    nao a chave de API: um cliente que gira chaves nao deve ganhar
  //    orcamento novo a cada rotacao.
  const { clienteId, chaveId, plano } = credencialVerificada;

  // 3) Rotas caras recebem escopo proprio. Sem isso, uma exportacao
  //    completa consome o orcamento que sustentaria mil consultas baratas.
  const grupoDeRota = classificarRota(req.method, req.routePattern);

  if (grupoDeRota === 'caro') {
    return {
      chave: `cli:${clienteId}:rota:${grupoDeRota}`,
      classe: plano,
      escopo: 'cliente+rota',
      chaveId,
    };
  }

  return {
    chave: `cli:${clienteId}`,
    classe: plano,
    escopo: 'cliente',
    chaveId,
  };
}

/**
 * Endereco de origem confiavel exige saber quantos proxies existem
 * a frente. Ler o primeiro valor de X-Forwarded-For sem essa conta e
 * aceitar o endereco que o cliente digitou.
 */
function enderecoDeOrigem(req) {
  const PROXIES_CONFIAVEIS = 1; // borda propria; ajuste por ambiente
  const cadeia = String(req.headers['x-forwarded-for'] || '')
    .split(',')
    .map((parte) => parte.trim())
    .filter(Boolean);

  if (cadeia.length > PROXIES_CONFIAVEIS) {
    return cadeia[cadeia.length - 1 - PROXIES_CONFIAVEIS];
  }
  return req.socket.remoteAddress;
}

function classificarRota(metodo, padrao) {
  const CARAS = new Set([
    'GET /v1/relatorios/:id/exportar',
    'POST /v1/buscas/avancada',
    'POST /v1/lotes/importar',
  ]);
  return CARAS.has(`${metodo} ${padrao}`) ? 'caro' : 'padrao';
}

O detalhe da função de endereço de origem é o que separa um limitador funcional de um que pode ser contornado em uma linha. Ler o primeiro elemento do cabeçalho de encaminhamento é o erro clássico, porque esse elemento é escrito pelo cliente e pode conter qualquer coisa. O valor confiável é contado a partir do fim, pulando exatamente o número de proxies que a própria infraestrutura coloca na frente, e esse número precisa ser configuração explícita, não suposição. Uma borda que ganha um balanceador novo sem atualizar essa contagem passa a limitar o endereço do balanceador, o que na prática limita todo mundo junto.

A escolha de usar o identificador do cliente em vez do identificador da chave de API também tem consequência operacional. Se o orçamento fosse por chave, um integrador poderia multiplicar a própria capacidade simplesmente emitindo mais chaves, e a rotação de credenciais, que é uma prática desejável, criaria orçamento extra como efeito colateral. Vale manter o identificador da chave nos rótulos de métrica, porque ele é o que permite descobrir qual sistema do cliente está gerando o excesso, mas ele não deve entrar na chave de contagem.

03

Quatro algoritmos, e por que a janela fixa entrega o dobro do pico

Com a chave resolvida, a escolha do algoritmo passa a ser sobre qual forma de tráfego o limite permite. Todos os quatro candidatos usuais respeitam a mesma média no longo prazo, e é por isso que a comparação feita apenas com a média não revela diferença nenhuma. A diferença está no formato da rajada que cada um deixa passar e no custo de manter o estado.

A janela fixa conta requisições dentro de um intervalo de relógio e zera a contagem quando o intervalo vira. Ela é a mais simples e tem um defeito que aparece sempre em produção: um cliente que envia todo o orçamento nos últimos instantes de uma janela e repete no primeiro instante da seguinte entrega o dobro do teto contratado dentro de um intervalo contínuo. Um teto de cem requisições por minuto vira duzentas em um segundo, e a borda considera as duas janelas dentro da política. Esse comportamento não é raro nem exige má intenção: qualquer cliente que sincroniza trabalho no início do minuto, o que é o padrão de agendadores, produz exatamente esse formato.

JANELA FIXA: o pico de fronteira

  teto = 100 req/min

  minuto 1                    | minuto 2
  ............................|............................
                       [100]  | [100]
                         ^         ^
                         |         |
                    59.6s |         | 60.2s

  intervalo continuo de 1 segundo -> 200 requisicoes
  contagem da janela 1 = 100 (dentro da politica)
  contagem da janela 2 = 100 (dentro da politica)

JANELA DESLIZANTE PONDERADA: a fronteira desaparece

  estimativa = contagem_atual + contagem_anterior * fracao_restante

  em 60.2s, fracao_restante da janela anterior = 0.997
  estimativa = 100 + 100 * 0.997 = 199.7 -> rejeita

BALDE DE FICHAS: rajada explicita e controlada

  capacidade = 20 fichas (rajada maxima)
  reposicao  = 100/60 fichas por segundo (taxa media)

  cliente ocioso acumula ate 20 e gasta de uma vez,
  depois volta a ser limitado pela taxa de reposicao.

A janela deslizante ponderada corrige a fronteira sem guardar a lista de carimbos de tempo de cada requisição. Ela mantém duas contagens, a da janela corrente e a da anterior, e estima o consumo como a contagem corrente somada à contagem anterior multiplicada pela fração da janela anterior que ainda está dentro do intervalo de observação. A estimativa não é exata, mas erra por pouco e por um fator conhecido, e custa dois inteiros por chave em vez de uma lista. É a escolha padrão quando o objetivo é fazer valer um número contratado com o mínimo de surpresa.

O balde de fichas resolve um problema diferente: ele permite rajada de propósito. A capacidade do balde é o tamanho da rajada que o cliente pode emitir depois de um período de ociosidade, e a taxa de reposição é a média sustentada. Isso é o que integrações reais querem, porque um sistema que processa um lote de pedidos a cada cinco minutos precisa de rajada, e a janela deslizante o trataria como abusivo. O balde vazante é o inverso: ele aceita rajada na entrada mas entrega numa taxa constante, o que o torna um enfileirador e não um rejeitador, e por isso ele aparece na seção sobre o que fazer com o excedente.

AlgoritmoEstado por chaveRajada permitidaFalha característica
Janela fixaUm contador e um instante de viradaAté duas vezes o teto na fronteiraPico de fronteira que o painel nunca mostra
Janela deslizante ponderadaDois contadoresPraticamente nenhuma, o teto é respeitadoEstimativa levemente conservadora com tráfego irregular
Balde de fichasSaldo e instante da última reposiçãoExplícita, igual à capacidade do baldeRajada mal dimensionada vira pico real na origem
Balde vazanteFila com tamanho máximoAbsorvida pela fila, nunca repassadaLatência cresce em silêncio até a fila encher

Na prática, os dois últimos não competem entre si: combinam. O balde de fichas decide o que é excesso e o balde vazante decide o que fazer com parte desse excesso. Um arranjo que funciona bem em API de integração usa fichas para o teto por cliente, com capacidade de rajada em torno de dez a vinte por cento do teto por minuto, e um pequeno enfileiramento com espera curta antes de rejeitar, o que absorve a rajada acidental de um agendador sem esconder o abuso sustentado.

04

O contador distribuído entre instâncias de borda

Um limitador em memória é correto enquanto existe uma instância de borda. Com seis instâncias atrás de um balanceador, cada uma passa a ver aproximadamente um sexto do tráfego do cliente e a aplicar o teto inteiro sobre essa fração, o que multiplica o limite efetivo pelo número de instâncias. Pior: o fator de multiplicação muda sozinho quando o autoescalonamento adiciona instâncias durante o pico, ou seja, o limite afrouxa exatamente quando deveria apertar.

A solução direta é um contador compartilhado, e a implementação que sobrevive a concorrência precisa ser atômica. Ler o valor, decidir e escrever de volta em três operações separadas produz condição de corrida sob carga, que é justamente o regime em que o limite importa. Em um armazenamento de chave e valor com execução de script, a decisão inteira roda do lado do servidor, o que também reduz o número de viagens de rede por requisição para uma.

-- Balde de fichas atomico em Lua, executado no Redis.
-- KEYS[1]  chave do cliente
-- ARGV[1]  capacidade do balde (rajada maxima)
-- ARGV[2]  taxa de reposicao em fichas por segundo
-- ARGV[3]  instante atual em milissegundos
-- ARGV[4]  fichas pedidas (custo da requisicao)
-- Retorna: { permitido, restante, espera_ms, reset_s }

local capacidade   = tonumber(ARGV[1])
local taxa         = tonumber(ARGV[2])
local agora        = tonumber(ARGV[3])
local pedido       = tonumber(ARGV[4])

local estado   = redis.call('HMGET', KEYS[1], 'fichas', 'ts')
local fichas   = tonumber(estado[1])
local ultimoTs = tonumber(estado[2])

if fichas == nil then
  fichas   = capacidade
  ultimoTs = agora
end

-- Reposicao proporcional ao tempo decorrido, limitada pela capacidade.
local decorrido = math.max(0, agora - ultimoTs) / 1000
fichas = math.min(capacidade, fichas + decorrido * taxa)

local permitido = 0
local espera = 0

if fichas >= pedido then
  fichas = fichas - pedido
  permitido = 1
else
  -- Quanto tempo falta para acumular o que esta faltando.
  espera = math.ceil(((pedido - fichas) / taxa) * 1000)
end

redis.call('HSET', KEYS[1], 'fichas', fichas, 'ts', agora)

-- Expiracao = tempo para reencher o balde do zero, com folga.
-- Sem isso, cada cliente que chamou uma vez fica na memoria para sempre.
local ttl = math.ceil(capacidade / taxa) + 10
redis.call('EXPIRE', KEYS[1], ttl)

local reset = math.ceil((capacidade - fichas) / taxa)
return { permitido, math.floor(fichas), espera, reset }

A linha da expiração é a que evita o vazamento lento que costuma passar despercebido por meses. Sem tempo de vida, cada cliente que chamou a API uma única vez deixa uma chave residente, e num serviço com chamadores efêmeros isso cresce até o armazenamento ficar sem memória. O tempo de vida correto é o tempo necessário para o balde voltar à capacidade cheia, porque a partir daí o estado guardado é indistinguível do estado inicial e pode ser descartado sem alterar nenhuma decisão.

Existe o custo da viagem até o armazenamento compartilhado em cada requisição, e ele é real. A saída usada em serviços de alto volume é um esquema de duas camadas: cada instância mantém um limitador local que autoriza uma fração do orçamento e sincroniza periodicamente com o contador central, pedindo um bloco de fichas em vez de uma ficha por vez. O limite passa a ser aproximadamente correto em vez de exatamente correto, com um erro máximo igual ao tamanho do bloco multiplicado pelo número de instâncias, o que é aceitável quando o teto é uma proteção e não uma cobrança. Quando o número é faturado, a aproximação deixa de ser aceitável e a viagem central volta a ser obrigatória.

A última decisão dessa camada é o que fazer quando o armazenamento compartilhado fica indisponível. Falhar fechado transforma uma indisponibilidade do limitador em indisponibilidade total da API, o que é desproporcional. Falhar aberto remove a proteção justamente durante um incidente de infraestrutura, que é quando a carga costuma estar anormal. O comportamento equilibrado é falhar para o limitador local de cada instância, com o orçamento dividido pelo número esperado de instâncias e um teto global de segurança acima dele, e registrar essa degradação como evento explícito para que ela não passe despercebida.

05

Rejeitar não é a única resposta, e o cabeçalho é parte do contrato

O tratamento do excedente costuma ser reduzido a uma decisão binária entre passar e rejeitar, e essa redução descarta as respostas mais úteis. Um excedente pode ser enfileirado por um curto período, pode ser servido a partir de um cache com dado ligeiramente mais antigo, pode ser degradado para uma versão mais barata da mesma operação, ou pode ser aceito e processado de forma assíncrona com um identificador de acompanhamento. Cada uma dessas respostas preserva a intenção do chamador em vez de descartá-la, e todas custam menos ao serviço do que o pico original.

  • Enfileirar com espera curta, na ordem de cinquenta a duzentos milissegundos, absorve a rajada de agendador sem esconder abuso sustentado, porque o abuso sustentado enche a fila e volta a rejeitar.
  • Servir do cache é a melhor resposta para leitura de dado que tolera alguns segundos de atraso, e transforma excedente em custo quase zero em vez de erro.
  • Degradar a operação, respondendo uma busca sem os campos derivados caros ou um relatório com granularidade menor, mantém o fluxo do cliente vivo e sinaliza a degradação no corpo da resposta.
  • Aceitar de forma assíncrona é o caminho natural para escrita em lote, e troca uma rejeição por um identificador que o cliente consulta depois.
  • Rejeitar com código 429 permanece a resposta correta para abuso sustentado e para operações que não têm versão barata nem assíncrona.

Quando a rejeição é a resposta, o que separa um limite utilizável de um limite hostil é a informação que acompanha a recusa. Um cliente que recebe apenas o código de status não tem como se comportar bem, e a reação previsível é tentar de novo imediatamente, o que aumenta a carga exatamente no momento de saturação. Os cabeçalhos padronizados de limitação existem para resolver isso e têm um formato que já é esperado por bibliotecas de cliente.

// Middleware de limitacao com cabecalhos que tornam o limite um contrato.
// Formato dos cabecalhos conforme o draft RateLimit do IETF, que e o que
// bibliotecas de cliente modernas ja sabem interpretar.

import { createClient } from 'redis';
import { readFile } from 'node:fs/promises';

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const script = await readFile(new URL('./balde.lua', import.meta.url), 'utf8');
const sha = await redis.scriptLoad(script);

const ORCAMENTO = {
  // teto por minuto e rajada por plano; a rajada fica em torno de 15%
  // do teto, suficiente para agendador e insuficiente para abuso.
  free:       { porMinuto: 60,   rajada: 10 },
  pro:        { porMinuto: 600,  rajada: 90 },
  enterprise: { porMinuto: 6000, rajada: 900 },
  anon:       { porMinuto: 20,   rajada: 5 },
};

export function limitadorPorCliente({ aoExceder = 'rejeitar' } = {}) {
  return async function middleware(req, res, next) {
    const { chave, classe } = resolverChaveDeLimite(req, req.credencial);
    const plano = ORCAMENTO[classe] || ORCAMENTO.free;
    const custo = custoDaRequisicao(req);   // 1 por padrao, maior em rota cara

    let resultado;
    try {
      resultado = await redis.evalSha(sha, {
        keys: [`rl:${chave}`],
        arguments: [
          String(plano.rajada),
          String(plano.porMinuto / 60),
          String(Date.now()),
          String(custo),
        ],
      });
    } catch (erro) {
      // Degradacao explicita: o limitador central caiu, nao a API.
      req.log.warn({ erro: erro.message }, 'limitador degradado para local');
      return limitadorLocal(req, res, next, plano);
    }

    const [permitido, restante, esperaMs, resetS] = resultado.map(Number);

    // Cabecalhos em toda resposta, nao apenas na rejeicao: o cliente bem
    // comportado precisa ver a folga encolhendo antes de bater no teto.
    res.setHeader('RateLimit-Limit', String(plano.porMinuto));
    res.setHeader('RateLimit-Remaining', String(Math.max(0, restante)));
    res.setHeader('RateLimit-Reset', String(resetS));
    res.setHeader('RateLimit-Policy', `${plano.porMinuto};w=60;burst=${plano.rajada}`);

    if (permitido === 1) return next();

    // Espera curta absorve rajada de agendador sem mascarar abuso.
    if (aoExceder === 'enfileirar' && esperaMs <= 200) {
      await new Promise((resolver) => setTimeout(resolver, esperaMs));
      return middleware(req, res, next);
    }

    const retryAfter = Math.max(1, Math.ceil(esperaMs / 1000));
    res.setHeader('Retry-After', String(retryAfter));
    return res.status(429).json({
      erro: 'rate_limit_excedido',
      // A mensagem diz o que fazer, nao apenas o que aconteceu.
      mensagem: `Orcamento de ${plano.porMinuto} requisicoes por minuto excedido. ` +
        `Tente novamente em ${retryAfter}s ou use o endpoint em lote.`,
      limite: plano.porMinuto,
      janelaSegundos: 60,
      tentarEmSegundos: retryAfter,
    });
  };
}

O detalhe de emitir os cabeçalhos em toda resposta, e não apenas na rejeição, é o que muda o comportamento do integrador. Um cliente que vê a folga encolhendo de quinhentos para cinquenta ao longo de trinta segundos tem como desacelerar sozinho antes do erro, e bibliotecas de cliente modernas fazem isso automaticamente quando os cabeçalhos estão presentes. Emitir a informação só no momento da recusa é equivalente a avisar do limite depois que ele já foi ultrapassado, o que serve para explicar e não para prevenir.

O cabeçalho de nova tentativa merece um cuidado específico. Se todos os clientes rejeitados receberem exatamente o mesmo valor, todos voltam exatamente no mesmo instante e produzem um pico sincronizado no fim da espera, que é o mesmo fenômeno de rebanho que derruba serviços depois de uma queda. A correção é adicionar uma dispersão aleatória de dez a vinte por cento sobre o valor calculado, de forma que o retorno seja distribuído em vez de simultâneo.

06

Os quatro indicadores que dizem se o limite protege ou empurra

Um limite de taxa é uma política, e políticas precisam de evidência para serem ajustadas. A métrica que quase todo mundo instrumenta primeiro, a contagem de respostas 429, é a menos útil isoladamente, porque ela sobe tanto quando o limite está protegendo corretamente quanto quando ele está apertado demais, e os dois casos exigem ações opostas. O que distingue um do outro é olhar a distribuição por cliente e a proximidade do teto entre os que nunca são rejeitados.

IndicadorO que ele respondeSinal de limite bem calibradoSinal de problema
Rejeições por cliente, não agregadasQuem está batendo no tetoConcentração em poucos chamadores conhecidosRejeição espalhada por muitos clientes pequenos
Utilização do orçamento no percentil noventa e cincoQuanto da fatia cada cliente usa no picoMaioria abaixo de setenta por centoVários clientes acima de noventa e cinco sem rejeitar ainda
Latência do próprio limitadorQuanto a proteção custa ao caminho de requisiçãoAbaixo de dois milissegundos no percentil noventa e noveCauda alta, indicando viagem de rede saturada
Tempo em modo degradadoQuanto o limite ficou aproximado por falha centralPróximo de zero, com eventos isoladosMinutos acumulados por dia sem ninguém saber

A segunda linha é a que permite agir antes do chamado. Um cliente que passa semanas usando noventa e cinco por cento do orçamento sem ser rejeitado está a um crescimento de dez por cento de virar um incidente, e essa é a hora de conversar sobre plano, sobre endpoint em lote ou sobre um aumento de fatia, e não depois que a integração dele quebrou. Esse indicador transforma o limite de taxa em uma ferramenta comercial além de técnica, porque ele mostra quem está prestes a precisar de mais capacidade.

A primeira linha resolve a pergunta que dá origem à seção inteira. Se as rejeições estão concentradas em poucos chamadores identificáveis, o limite está fazendo exatamente o que foi projetado para fazer: isolar o excesso na fatia de quem o produziu. Se elas estão espalhadas por muitos clientes pequenos, o teto está abaixo do uso legítimo e a política precisa ser revista, porque nesse regime o limite deixou de proteger o serviço e passou a ser a principal causa de erro que os clientes enxergam.

  1. Instrumentar antes de limitar: rodar o limitador em modo de observação por uma ou duas semanas, calculando a decisão e registrando a métrica sem rejeitar nada.
  2. Definir o teto de cada plano no percentil noventa e nove do uso observado, com folga, para que o limite inicial não rejeite nenhum comportamento que já era normal.
  3. Habilitar a rejeição primeiro para a classe anônima e para as rotas caras, que é onde o risco é maior e o impacto em cliente legítimo é menor.
  4. Publicar os limites na documentação junto com os cabeçalhos emitidos, porque um limite não documentado é indistinguível de uma instabilidade do ponto de vista do integrador.
  5. Habilitar a rejeição por cliente autenticado, acompanhando a distribuição de rejeições por chamador durante os primeiros dias.
  6. Revisar mensalmente a utilização no percentil noventa e cinco por cliente e ajustar fatias antes que a rejeição apareça.

FAQ

Perguntas frequentes

Como definir o valor inicial do teto por cliente sem chutar um número redondo?

O caminho que evita tanto o limite inútil quanto o limite hostil é derivar o número do uso observado antes de derivar da capacidade. A primeira etapa é rodar o limitador em modo de observação, calculando a decisão completa e registrando a métrica de utilização sem rejeitar nada, por um período que cubra pelo menos um ciclo de negócio inteiro, o que em integrações costuma ser um mês porque existe pico de fechamento. Com essa amostra, o teto inicial de cada classe sai do percentil noventa e nove do uso por cliente dentro da classe, multiplicado por uma folga de trinta a cinquenta por cento. Esse número tem a propriedade de não rejeitar nada que já era comportamento normal, o que é fundamental para que a ativação do limite não seja confundida com uma degradação do serviço. A segunda etapa é confrontar a soma dos tetos com a capacidade real, e aqui aparece o fato incômodo: a soma dos orçamentos individuais quase sempre excede a capacidade do serviço, porque nem todos os clientes atingem o pico ao mesmo tempo. Isso é aceitável e é exatamente a razão de existir um teto global acima dos individuais, funcionando como válvula de último recurso. O que não é aceitável é que a soma exceda a capacidade em uma ordem de grandeza, porque nesse caso os limites individuais nunca serão atingidos e a proteção efetiva volta a ser o teto global, com todo o dano colateral que ele produz. Quando a conta não fecha, a saída é criar classes com tetos diferenciados e mover o custo para o plano, não achatar todo mundo no mesmo número.

Vale a pena limitar por custo estimado em vez de por número de requisições?

Vale quando a variação de custo entre operações passa de uma ordem de grandeza, e não vale quando a carga é homogênea, porque a unidade de custo precisa ser explicada ao integrador e uma unidade que ninguém entende gera mais chamado do que protege. O critério prático é medir a distribuição de tempo de processamento por rota: se o percentil noventa e cinco da rota mais cara for até dez vezes o da rota mais barata, contar requisições com um custo maior para as rotas caras já resolve, e essa é a abordagem do exemplo deste artigo, onde a rota cara consome mais fichas do mesmo balde. Se a diferença for de cem vezes ou mais, como acontece entre uma consulta por identificador e uma exportação completa, contar requisições vira ficção e o limite precisa ser expresso em uma unidade que reflita trabalho, seja tempo de processamento, seja linhas retornadas, seja uma unidade sintética publicada na documentação. O ponto de atenção da limitação por custo é que o custo real só é conhecido depois de executar, o que obriga a um esquema de reserva e acerto: cobrar uma estimativa antes da execução e devolver ou cobrar a diferença ao final. Isso é o mesmo mecanismo usado em teto de gasto por cliente e tem o mesmo cuidado, que é garantir que a devolução aconteça mesmo quando a requisição falha no meio, sob pena de o cliente ficar pagando por trabalho que nunca foi feito. Uma alternativa mais simples, e suficiente na maioria dos casos, é manter o limite em requisições e criar um limite separado e paralelo por concorrência, restringindo quantas operações caras cada cliente pode ter em execução simultânea, o que protege a capacidade sem exigir nenhuma unidade nova.

O que muda no limite de taxa quando a API é usada por um front-end do próprio produto e não só por integradores?

Muda a identidade que faz sentido contar e muda o significado de uma rejeição. Um front-end distribui a mesma credencial de aplicação entre todos os usuários finais, então limitar por cliente colocaria milhares de pessoas dentro de um único orçamento, e a primeira rajada de uso normal derrubaria a aplicação inteira. A chave correta nesse caso é composta, combinando o identificador da aplicação com o identificador do usuário autenticado, o que dá a cada sessão o seu próprio orçamento e mantém a possibilidade de um teto agregado por aplicação acima dele. Para tráfego não autenticado do próprio produto, como uma página pública com busca, a identidade volta a ser de rede e o orçamento precisa ser pequeno, complementado por controles que não são de taxa, como prova de trabalho leve ou verificação de origem, porque um limite por endereço é contornável com muitos endereços. O segundo ponto é o significado da rejeição: para um integrador, um 429 é uma instrução operacional que a biblioteca dele trata, enquanto para um usuário final é uma tela de erro que ele não tem como resolver. Isso torna as respostas alternativas muito mais valiosas no caminho do produto, e é onde servir do cache, degradar a operação ou simplesmente desacelerar a interface com uma indicação de carregamento valem mais do que qualquer código de status. Vale ainda separar fisicamente os dois caminhos, com limites, políticas e até pontos de entrada distintos para tráfego de produto e tráfego de integração, porque eles têm formatos de carga diferentes e misturá-los força um compromisso que penaliza os dois.

O limite por cliente troca dano colateral por previsibilidade contratada

Um teto global contém a carga e distribui a rejeição por quem estava chegando, o que faz o cliente certo pagar pelo comportamento do errado e torna a qualidade de serviço impossível de documentar. Resolver a identidade do chamador antes de contar, escolher o algoritmo pelo formato de rajada que ele permite, manter o contador atômico e compartilhado com degradação explícita, e emitir os cabeçalhos em toda resposta transformam o limite em um contrato que o integrador consegue respeitar sozinho. Posso desenhar as classes de orçamento a partir do uso real da sua API, implementar o limitador distribuído com as respostas alternativas ao excedente, definir a política de degradação quando o contador central falha e configurar os quatro indicadores que mostram se o limite está protegendo o serviço ou apenas empurrando o problema para o cliente.