Pular para o conteúdo
16 min de leitura

O que é rate limiting: como proteger uma API do abuso e de si mesma

Por Lucas Andrade ·

Rate limiting limita quantas requisições um cliente faz num intervalo. Conheça token bucket, sliding window, o 429 e onde aplicar cada um.

Neste artigo

Toda API que sobrevive o suficiente para ter usuários reais chega, mais cedo ou mais tarde, à mesma pergunta: o que acontece quando alguém manda requisições demais? "Alguém" nem precisa ser mal-intencionado — pode ser um cliente com um bug de retry em loop, um script de scraping legítimo rodando rápido demais, ou o próprio time interno testando algo em produção sem perceber o volume. Rate limiting é a resposta estrutural a essa pergunta: um limite explícito de quantas requisições uma origem pode fazer num intervalo de tempo, aplicado de forma consistente, com uma resposta previsível para quem o estoura.

A tentação é tratar isso como detalhe de infraestrutura, algo que se resolve com uma linha de configuração num proxy e nunca mais se pensa a respeito. Na prática, a escolha de algoritmo, de onde aplicar o limite e de como identificar quem está fazendo a requisição carrega decisões de arquitetura que se pagam ou cobram caro conforme o sistema cresce. Este artigo cobre o problema que o rate limiting resolve, os algoritmos clássicos usados para resolvê-lo, e as decisões práticas — identificação de cliente, lugar de aplicação, resposta ao estouro — que separam uma implementação correta de uma que parece funcionar até o primeiro pico de tráfego real.

Por que limitar a taxa de requisições#

Existem pelo menos cinco motivos distintos para impor um limite, e vale separá-los porque cada um aponta para um tipo de configuração diferente.

Proteção contra abuso ativo. Um atacante tentando adivinhar uma senha por força bruta depende de conseguir testar milhares de combinações rapidamente; um limite de poucas tentativas por minuto num endpoint de login torna o ataque impraticável sem impedir o uso legítimo. O mesmo vale para scraping agressivo de conteúdo e para tentativas de enumerar recursos sequencialmente. O rate limit não impede o abuso por completo, mas eleva o custo dele a um patamar que desestimula a maioria dos atacantes.

Justiça entre clientes (fairness). Numa API compartilhada por múltiplos clientes — tenants, contas, chaves de API —, nada garante que o tráfego se distribua de forma equilibrada. Um cliente com um bug, um pico legítimo de uso ou simplesmente mais volume de negócio pode, sem limite algum, consumir a capacidade toda do serviço e degradar a experiência de todos os outros. Limitar por cliente é o que garante que o barulho de um não vire silêncio para o resto.

Proteção de recursos a jusante. A API raramente é o gargalo real; o banco de dados, um serviço de terceiros com sua própria cota, ou uma fila que processa em sequência costumam ser bem mais frágeis do que a camada HTTP que os expõe. Um rate limit na borda evita que um pico de requisições se propague para trás e derrube ou sature um componente que não foi desenhado para aquele volume.

Controle de custo. Toda chamada a uma API de terceiro cobrada por uso, todo processamento que consome CPU ou tempo de um modelo de IA, tem um custo direto por requisição. Sem limite, o custo de operação vira uma função do comportamento do cliente mais agressivo, não uma função planejada. Limitar a taxa é também limitar o gasto máximo possível num intervalo.

Proteção contra si mesmo. Este é o motivo mais fácil de subestimar. Um cliente interno com um bug de retry sem backoff, um cron mal configurado disparando a cada segundo em vez de a cada hora, um teste automatizado apontando para produção por engano — nenhum desses é um ataque, mas o efeito no servidor é indistinguível de um. Um sistema sem rate limiting nenhum está, na prática, confiando que todo cliente presente e futuro vai se comportar perfeitamente. Essa aposta perde eventualmente.

Rate limiting, throttling e load shedding#

Os três termos aparecem misturados com frequência, mas descrevem estratégias diferentes para o mesmo problema geral de excesso de carga.

Rate limiting rejeita acima de um limite fixo por identidade. A regra é simples e previsível: "esse cliente pode fazer N requisições por minuto"; a requisição N+1 é recusada, ponto. É a estratégia mais comum porque é a mais fácil de comunicar ao cliente e de testar.

Throttling desacelera em vez de rejeitar. Em vez de recusar o excesso, o sistema atrasa a resposta ou reduz a prioridade da requisição, deixando-a passar mais devagar. É útil quando o objetivo é suavizar picos sem devolver erro, mas exige que o cliente tolere latência variável — o que nem sempre é aceitável.

Load shedding descarta com base na saúde do sistema, não num contador por cliente. Quando o servidor detecta que está sobrecarregado — fila crescendo, latência subindo, CPU no teto —, ele passa a recusar uma fração das requisições, muitas vezes as de menor prioridade, independentemente de qual cliente as fez. É uma proteção de última linha, reativa ao estado real do sistema, enquanto o rate limiting é proativo e baseado em regras definidas de antemão. Os dois não são substitutos um do outro: um sistema robusto normalmente tem rate limiting por cliente como primeira linha e load shedding como rede de segurança para quando, mesmo com os limites em vigor, o agregado ainda ultrapassa a capacidade.

Os algoritmos clássicos#

A pergunta "quantas requisições esse cliente já fez nesse intervalo?" parece trivial, mas a forma de contar tem implicações reais de precisão, memória e comportamento na borda entre janelas. Cinco algoritmos cobrem praticamente todo o espaço de soluções usadas em produção.

Fixed Window Counter#

A abordagem mais simples: divide o tempo em janelas fixas — por exemplo, de minuto em minuto — e mantém um contador por cliente, zerado no início de cada janela. Chegou a requisição, incrementa o contador; se o contador ultrapassa o limite, rejeita.

A vantagem é a simplicidade e o custo mínimo de memória — um único inteiro por cliente por janela. A desvantagem é o problema da borda (boundary problem): nada impede que um cliente faça o limite inteiro de requisições no último segundo de uma janela e o limite inteiro de novo no primeiro segundo da janela seguinte. Do ponto de vista do cliente, isso é o dobro do limite nominal concentrado numa janela de dois segundos — o contador reseta, mas o sistema a jusante não sabe disso e sente o pico cheio.

Sliding Window Log#

Em vez de um contador por janela fixa, guarda o timestamp de cada requisição individual num log (tipicamente uma lista ordenada por cliente). Para decidir se uma nova requisição é permitida, remove do log tudo que é mais velho que a janela e conta quantas entradas sobraram.

É o mais preciso dos cinco — nenhuma aproximação, o limite é respeitado exatamente sobre qualquer janela deslizante de tempo. O custo é memória proporcional ao número de requisições dentro da janela, o que fica caro em limites altos ou janelas longas, porque cada requisição individual precisa ser armazenada e depois expurgada.

Sliding Window Counter#

Uma aproximação que resolve o problema da borda do Fixed Window sem pagar o custo de memória do Sliding Window Log. Mantém dois contadores de janela fixa — o atual e o anterior — e estima a taxa efetiva ponderando o contador anterior pela fração da janela anterior que ainda "está dentro" da janela deslizante equivalente.

Por exemplo, se a janela é de um minuto e já se passaram 20 segundos da janela atual, a estimativa pesa 40/60 do contador da janela anterior somado ao contador da janela atual. É uma aproximação, não é exata como o log, mas suaviza a rajada de borda a um custo de memória praticamente igual ao Fixed Window — dois contadores em vez de um. Na prática, é o equilíbrio que a maioria dos gateways usa quando não adotam token bucket.

Token Bucket#

O algoritmo mais usado em produção, e vale entender com cuidado porque o raciocínio por trás dele aparece em praticamente todo limitador sério.

A ideia é um balde com capacidade fixa de tokens. O balde enche a uma taxa constante — digamos, 10 tokens por segundo — até o limite da sua capacidade máxima. Cada requisição que chega precisa gastar um token do balde para ser processada; se o balde está vazio, a requisição é rejeitada (ou espera, dependendo da variante). Se o balde está cheio e nenhuma requisição chega por um tempo, os tokens simplesmente se acumulam até o teto da capacidade.

A propriedade que torna o token bucket especial é permitir rajadas controladas. Um cliente que ficou inativo por um tempo acumulou tokens no balde e pode gastar todos eles de uma vez, numa rajada rápida, sem ser penalizado — desde que não ultrapasse a capacidade do balde. Depois da rajada, ele volta a ser limitado pela taxa de reposição. Isso modela bem o comportamento real de clientes: uma aplicação que faz uma sincronização inicial pesada e depois só requisições esporádicas se beneficia de poder gastar a rajada acumulada, em vez de ser limitada uniformemente segundo a segundo como o Fixed Window faria.

Conceitualmente, a verificação por requisição é barata e pode ser expressa assim:

``` // Estado por cliente: tokens atuais e timestamp da última reposição struct Bucket { tokens: f64, last_refill: Instant, }

// Chamado a cada requisição recebida do cliente fn allow_request(bucket: &mut Bucket, capacity: f64, refill_rate: f64) -> bool { let now = Instant::now(); let elapsed = now.duration_since(bucket.last_refill).as_secs_f64();

// Repõe tokens proporcionalmente ao tempo decorrido, sem passar da capacidade bucket.tokens = (bucket.tokens + elapsed * refill_rate).min(capacity); bucket.last_refill = now;

if bucket.tokens >= 1.0 { bucket.tokens -= 1.0; true // consome um token, requisição passa } else { false // balde vazio, requisição é rejeitada } } ```

Note que a reposição não precisa de um processo em segundo plano rodando a cada segundo — ela é calculada de forma preguiçosa (lazy), com base no tempo decorrido desde a última verificação, no momento em que uma nova requisição chega. Isso mantém o algoritmo barato mesmo com um número grande de clientes inativos: nenhum trabalho é feito para quem não está fazendo requisições.

Leaky Bucket#

Uma variação com a intuição invertida do token bucket. Em vez de um balde que enche e cada requisição gasta um token, o leaky bucket enfileira as requisições que chegam e as processa (a "saída" do balde) a uma taxa constante e fixa, como um balde furado vazando a um ritmo estável independentemente de quão rápido a água entra. Se o balde (a fila) enche, novas requisições transbordam e são descartadas.

A diferença prática em relação ao token bucket é que o leaky bucket suaviza a saída, não permite rajada. Onde o token bucket deixa passar uma rajada acumulada de uma vez, o leaky bucket força até a rajada a sair a um ritmo constante — útil quando o consumidor a jusante realmente não tolera picos, como um hardware físico ou uma fila de processamento síncrono, mas menos flexível para tráfego de API onde rajadas ocasionais de um cliente legítimo são normais e desejáveis.

Como identificar o cliente a limitar#

Um rate limit só faz sentido se souber contra qual identidade contar. Errar a identificação é uma das formas mais comuns de um rate limit falhar silenciosamente.

Por chave de API é a opção mais confiável quando existe. A chave já é a unidade de cobrança e de contrato na maioria das APIs públicas, então limitar por ela alinha o rate limit com o próprio modelo de negócio — cada chave tem seu orçamento de requisições, independente de onde a chamada se origina.

Por ID de usuário autenticado funciona bem para APIs internas ou orientadas a conta. Depois que o usuário se autentica, o identificador estável extraído do token validado — nunca de um campo enviado pelo cliente — se torna a chave do contador. É o equivalente, no rate limiting, do princípio de nunca confiar em identidade que vem do corpo da requisição.

Por IP é a opção mais fraca e mais traiçoeira, embora seja a mais tentadora por não exigir autenticação. O problema é que IP não identifica um cliente de forma confiável: várias organizações escondem centenas de usuários atrás de um único IP via NAT, então limitar por IP pode bloquear uma empresa inteira pelo comportamento de uma pessoa. Provedores de VPN e proxies corporativos agravam o mesmo efeito. Do lado oposto, o IPv6 é abundante o suficiente para um atacante rotacionar endereços com facilidade, tornando o limite por IP fácil de contornar por quem realmente quer abusar do sistema. Na prática, IP costuma servir como camada adicional de defesa — por exemplo, para endpoints não autenticados como login e cadastro — nunca como identificador único de um cliente que já tem uma forma melhor de se identificar.

Onde aplicar o limite#

Na borda (gateway, API gateway, CDN) é o lugar mais barato e mais cedo possível para rejeitar excesso. Rejeitar ali evita que a requisição sequer chegue à aplicação, poupando conexão, autenticação e qualquer trabalho que a aplicação faria antes de decidir recusar. É onde limites gerais e por IP costumam viver.

Na aplicação é onde limites com lógica de negócio mais fina se encaixam — por plano de assinatura, por endpoint específico com custo desproporcional (uma busca pesada versus uma leitura simples), ou por regras que dependem de estado que só a aplicação conhece. Nenhuma das duas camadas exclui a outra; um limite geral na borda como primeira linha, com limites mais específicos na aplicação, é uma combinação comum.

O ponto que mais gente esquece é a natureza distribuída do problema. Assim que a aplicação roda em mais de uma instância — o caso normal em produção —, um contador guardado em memória local de cada instância deixa de fazer sentido: se o limite é 100 requisições por minuto e há cinco instâncias, cada uma com seu próprio contador em memória, o cliente na prática consegue 500 requisições por minuto, cinco vezes o limite pretendido, simplesmente distribuindo a carga entre elas (o que um balanceador de carga faz de forma transparente, sem que o cliente precise cooperar). A correção é mover o estado do contador para um armazenamento compartilhado entre as instâncias — Redis é a escolha usual, por ser rápido o suficiente para uma operação por requisição e ter primitivas atômicas (INCR, scripts Lua) que evitam condições de corrida no incremento concorrente. Sem esse compartilhamento, o rate limit existe no papel e não existe na prática.

A resposta correta ao estourar o limite#

Quando uma requisição é rejeitada por exceder o limite, o código de status correto é 429 Too Many Requests — não 403 (que sugere problema de permissão, não de volume) nem 503 (que sugere indisponibilidade do serviço, não uma decisão deliberada sobre aquele cliente).

A resposta deve vir acompanhada de headers que permitam ao cliente se comportar corretamente, sem precisar adivinhar:

  • Retry-After: quantos segundos o cliente deve esperar antes de tentar de novo. É o header mais importante do conjunto — sem ele, o cliente só pode chutar quando tentar novamente.
  • RateLimit-Limit: o limite total configurado para aquela janela ou balde.
  • RateLimit-Remaining: quantas requisições ainda restam antes do próximo bloqueio.
  • RateLimit-Reset: quando o contador reseta ou quando o balde volta a ter tokens suficientes.

Devolver esses headers em toda resposta — não só na que estoura o limite — permite que um cliente bem-comportado se module antes mesmo de bater no limite, reduzindo o ritmo quando percebe que RateLimit-Remaining está baixo, em vez de descobrir o problema só pelo primeiro 429.

Boas práticas do lado do cliente#

Um rate limit bem desenhado do lado do servidor só cumpre o papel inteiro se o cliente reage de forma sensata ao 429, o que nem sempre acontece por padrão.

Respeitar o Retry-After em vez de tentar de novo imediatamente é o mínimo esperado. Ignorá-lo e retentar sem pausa transforma um bloqueio temporário numa sequência ininterrupta de requisições rejeitadas, que continua consumindo capacidade do servidor mesmo sem produzir nenhuma resposta útil.

Backoff exponencial com jitter é o padrão para retries em geral, e vale duplamente aqui. Em vez de esperar um intervalo fixo, o cliente dobra a espera a cada tentativa malsucedida (1s, 2s, 4s, 8s…) e soma um componente aleatório pequeno (o jitter) para evitar que múltiplos clientes que foram bloqueados ao mesmo tempo sincronizem suas retentativas e gerem um novo pico coordenado no exato momento em que o limite libera.

Nunca retentar em loop cego sem limite de tentativas. É exatamente esse padrão — um cliente que insiste indefinidamente sem backoff — que aparece na lista de motivos para ter rate limiting em primeiro lugar; um cliente bem escrito reconhece quando parar de tentar e propaga o erro para quem chamou, em vez de martelar o servidor para sempre.

Limites por tier e a diferença entre limitar e enfileirar#

APIs comerciais raramente usam um único limite global; o comum é atrelar o limite ao plano contratado — um tier gratuito com um teto baixo, tiers pagos com tetos progressivamente maiores, e eventualmente um tier enterprise com limite negociado ou ausente. Tecnicamente isso é só um parâmetro a mais na identificação do cliente: a chave de API já carrega, ou permite consultar, qual limite se aplica àquela conta.

Vale por fim distinguir limitar de enfileirar, porque os dois às vezes se confundem como soluções para o mesmo sintoma. Limitar rejeita o excesso e devolve a decisão ao cliente, que decide se e quando retentar — é uma política de admissão. Enfileirar aceita a requisição, mas adia o processamento, garantindo que ela eventualmente será atendida, geralmente às custas de latência maior e sem garantia de ordem de chegada estrita sob carga. Para requisições que precisam de resposta síncrona rápida, limitar é o caminho certo; para trabalho que pode esperar — processamento em lote, envio de notificações, geração de relatórios —, enfileirar costuma ser a escolha mais amigável ao cliente, porque ninguém recebe um erro por ter pedido algo legítimo num momento de pico.

Fechando#

Rate limiting não é uma feature de segurança isolada, é uma decisão estrutural sobre como o sistema se comporta sob volume — inclusive sob o próprio volume mal-comportado que ele mesmo pode gerar por um bug de retry ou um cron errado. Escolha o algoritmo pensando no comportamento que você quer permitir: fixed window se a simplicidade basta e a borda não importa, sliding window counter quando a borda importa mas o log completo é caro demais, token bucket quando rajadas legítimas fazem parte do uso normal, leaky bucket quando o consumidor a jusante exige saída constante. Identifique o cliente pela chave ou pelo usuário autenticado, nunca só pelo IP. Compartilhe o contador entre instâncias — em Redis, tipicamente — ou o limite não existe de fato num sistema distribuído. E devolva um 429 com headers que digam ao cliente exatamente quando tentar de novo, porque um limite sem essa informação transfere para o cliente um problema que o servidor já resolveu e simplesmente não contou.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly