System Design

Rate Limiting

Controlar a taxa de requisições que um cliente pode fazer a um serviço em um dado período — protegendo contra abuso, DDoS de baixa intensidade e consumo excessivo de recursos compartilhados.

Intenção

Rate limiting impõe um teto à velocidade com que um cliente pode consumir um serviço, garantindo que nenhum cliente — por engano ou má-fé — possa monopolizar os recursos e degradar a experiência dos demais.

Sem rate limiting, um único cliente que dispara um loop infinito de requisições, um bot de scraping agressivo ou um ataque volumétrico pode saturar completamente a capacidade de processamento do serviço, tornando-o indisponível para todos. Rate limiting é a primeira linha de defesa contra esse tipo de sobrecarga — mais simples e mais eficaz do que dimensionar a infraestrutura para absorver qualquer pico possível.

Problema

Serviços expostos a múltiplos clientes enfrentam desafios que rate limiting resolve diretamente:

  • Abuso e raspagem (scraping): bots que fazem milhares de requisições por segundo para extrair dados, testar senhas por força bruta ou simplesmente sobrecarregar o serviço.
  • Clientes com bugs: um cliente que entra em loop de retry sem backoff pode gerar mais requisições do que toda a base de clientes normal combinada.
  • Consumo desproporcional de APIs pagas: num modelo de custo por requisição, um único cliente com comportamento errático pode gerar custos inesperados para o provedor.
  • Proteção de endpoints custosos: geração de relatórios, envio de e-mails, consultas de banco complexas — operações que consomem recursos significativos precisam ser limitadas independentemente das demais.

Como funciona

Algoritmos principais

Token Bucket

Um bucket armazena até N tokens (capacidade máxima). Tokens são adicionados a uma taxa fixa (ex.: 10 tokens por segundo). Cada requisição consome um token; se o bucket estiver vazio, a requisição é rejeitada. Se o bucket estiver cheio e nenhuma requisição chegar, tokens acumulam até o limite máximo.

Vantagem: permite bursts legítimos — um cliente inativo acumula tokens e pode fazer N requisições em rajada quando precisar. Desvantagem: um burst até N pode acontecer a qualquer momento, o que pode sobrecarregar o serviço downstream que não está preparado para picos.

Leaky Bucket

Requisições entram numa fila e são processadas a uma taxa constante (ex.: 10 req/segundo), independente do ritmo de chegada. Se a fila encher, novas requisições são rejeitadas. A "torneira" vaza em ritmo constante — daí o nome.

Vantagem: suaviza bursts, protegendo o sistema downstream de picos. Desvantagem: requisições válidas em burst esperam na fila, aumentando a latência percebida.

Fixed Window Counter

Conta requisições dentro de uma janela fixa (ex.: 100 req por minuto, janela reseta todo minuto no segundo :00). Simples de implementar com um contador atômico e TTL.

Problema clássico: um cliente pode fazer 100 requisições nos últimos 5 segundos de uma janela e 100 nos primeiros 5 segundos da próxima — 200 requisições em 10 segundos, o dobro do limite pretendido.

Sliding Window Log

Registra o timestamp de cada requisição numa lista. Para cada nova requisição, remove da lista os timestamps mais antigos que N segundos e conta os restantes. Se o count excede o limite, rejeita. Mais preciso que Fixed Window, mas consome mais memória (um timestamp por requisição por cliente).

Sliding Window Counter

Híbrido eficiente: mantém o contador da janela atual e da janela anterior, calculando uma aproximação proporcional ao overlap com a janela anterior. Ex.: se a janela anterior teve 80 requisições, a janela atual tem 20 e estamos a 30% da janela atual, o contador estimado é 80 × 0.7 + 20 = 76.

Muito usado em produção: memória constante (dois contadores por cliente), sem a vulnerabilidade de burst da Fixed Window e com boa precisão.

Token Bucket bucket: 6/10 tokens +10 tokens/s requisição -1 token 200 OK bucket vazio 429 Permite burst: cliente inativo acumula tokens e pode fazer N requisicoes em rajada Fixed Window — Problema de Burst Janela 1 (0:00 – 0:59) Janela 2 (1:00 – 1:59) 1:00 100 req 0:55–0:59 100 req 1:00–1:04 ✓ 100 aceitas ✓ 100 aceitas 200 requisicoes em 10 segundos! Limite: 100/min — mas ambas as janelas validam Solucao: Sliding Window Counter aproxima a contagem com overlap ponderado da janela anterior Headers de resposta HTTP (RFC 6585 + padrão de mercado) HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 Retry-After: 42
Token Bucket permite bursts controlados (tokens acumulados). Fixed Window Counter tem a vulnerabilidade clássica de aceitar o dobro do limite em dois segundos adjacentes na virada de janela — o Sliding Window Counter resolve isso.

Dimensões de rate limiting

Rate limiting pode ser aplicado em múltiplas dimensões, combinadas conforme o caso de uso:

  Por IP           — proteção básica contra bots; penaliza usuários
                     atrás de NAT corporativo (ver armadilhas)
  Por usuário      — limite por conta autenticada; mais justo
  Por API key      — controle granular para integrações e parceiros
  Por endpoint     — limites diferentes para /login (5/min) vs
                     /search (100/min)
  Por tenant       — em SaaS multi-tenant, limitar por organização
  Combinado        — por IP E por usuário E por endpoint simultaneamente

Implementação com Redis

Redis é o padrão para rate limiting em ambientes distribuídos (múltiplas instâncias de servidor). O contador precisa ser compartilhado — se cada instância mantiver o próprio contador, o cliente pode fazer N requisições por instância, multiplicando o limite pelo número de instâncias.

  -- Fixed Window com Redis (Lua script para atomicidade)
  local key = "rl:" .. client_id .. ":" .. window_start
  local count = redis.call("INCR", key)
  if count == 1 then
    redis.call("EXPIRE", key, window_size_seconds)
  end
  if count > limit then
    return 429
  end
  return 200

Quando usar

  • APIs públicas: qualquer API exposta à internet deve ter rate limiting. Sem ele, um único cliente mal-configurado ou mal-intencionado pode derrubar o serviço inteiro.
  • Endpoints de autenticação: limitação agressiva em /login, /forgot-password e verificação de OTP é a proteção básica contra força bruta — 5 tentativas por minuto por IP é um ponto de partida razoável.
  • Operações custosas: geração de relatórios em PDF, exportação de dados, envio de e-mails em massa, chamadas a APIs externas pagas. Rate limiting protege tanto o custo operacional quanto a disponibilidade.

Quando ter cuidado

  • Rate limiting muito agressivo em APIs internas: serviços internos que chamam uns aos outros em alta frequência podem sofrer degradação artificial por limites mal dimensionados. Monitorar e calibrar com base em dados reais de produção antes de reduzir limites.
  • Limitar apenas por IP em contextos corporativos: centenas de funcionários de uma empresa podem compartilhar um único IP público — rate limiting por IP os trata como um único cliente. Prefira limitar por usuário autenticado nesses contextos.

Prós e contras

Prós

  • Protege a disponibilidade do serviço contra abuso, bugs de cliente e ataques volumétricos de baixa intensidade.
  • Garante equidade entre clientes: nenhum cliente individual monopoliza os recursos compartilhados.
  • Protege custos operacionais em APIs pagas por requisição ou por operação custosa.
  • Relativamente simples de implementar com Redis e idiomático em API Gateways (Kong, AWS API Gateway, Nginx).

Contras

  • Clientes legítimos podem ser penalizados se os limites forem mal dimensionados ou se compartilharem IP com outros clientes.
  • Não é defesa suficiente contra ataques DDoS volumétricos de alta intensidade — para isso, WAF e CDN com proteção DDoS são necessários.
  • Requer estado distribuído (Redis) em ambientes com múltiplas instâncias, adicionando latência e dependência de infraestrutura.
  • Limites fixos não se adaptam a variações legítimas de carga — um algoritmo mais sofisticado (ex.: Token Bucket com burst generoso) é mais justo.

Armadilhas comuns

1. Fixed Window com burst na virada

O problema mais clássico de rate limiting: 100 requisições nos últimos 5 segundos de uma janela mais 100 nos primeiros 5 segundos da próxima somam 200 em 10 segundos, embora o limite seja 100/minuto. Use Sliding Window Counter para mitigar — é a opção com melhor equilíbrio entre precisão, custo de memória e simplicidade de implementação.

2. Rate limiting por IP em redes corporativas

Uma empresa com 500 funcionários pode ter todos eles saindo pela mesma faixa de IP pública via NAT. Rate limiting agressivo por IP faz com que um funcionário que faz requisições normais seja bloqueado por culpa do comportamento de um colega. Sempre ofereça limites por usuário autenticado como alternativa.

3. Sem headers informativos na resposta 429

Uma resposta 429 Too Many Requests sem headers informativos deixa o cliente às cegas: ele não sabe quantas requisições restam, quando o limite reseta ou quanto tempo esperar. O resultado são retry loops com backoff arbitrário ou, pior, retry imediato que piora a sobrecarga.

Sempre inclua: Retry-After (segundos até poder tentar de novo), X-RateLimit-Limit (limite configurado), X-RateLimit-Remaining (requisições restantes na janela atual) e X-RateLimit-Reset (timestamp Unix do próximo reset).

4. Rate limiting só na borda sem proteção nos serviços internos

Se o rate limiting existe apenas no API Gateway, serviços internos que se chamam diretamente (sem passar pelo gateway) ficam desprotegidos. Um bug em um serviço A que faz chamadas em loop para o serviço B pode derrubar B mesmo com o gateway protegendo o tráfego externo. Considere rate limiting em nível de serviço para comunicação interna crítica.

Arquiteturas e padrões relacionados

O API Gateway é o local mais comum para implementar rate limiting em microsserviços — centralizando a política em um único ponto em vez de duplicar a lógica em cada serviço. Kong, AWS API Gateway, NGINX e Traefik oferecem rate limiting como funcionalidade nativa configurável.

Circuit Breaker e Rate Limiting são complementares: o Rate Limiting protege um serviço de sobrecarga causada por muitos clientes, enquanto o Circuit Breaker protege um cliente de continuar chamando um serviço que já está com falha. Juntos, formam uma camada robusta de resiliência.

Retries após receber 429 devem respeitar o Retry-After e ser idempotentes — o servidor deve estar preparado para receber a mesma requisição mais de uma vez sem efeitos colaterais indesejados.