Como implementar rate limiting em APIs: estratégias para proteger sem bloquear usuários legítimos

1. Fundamentos do Rate Limiting e seus Objetivos

1.1. O que é rate limiting e por que ele é crítico para APIs modernas

Rate limiting é uma técnica de controle de tráfego que restringe o número de requisições que um cliente pode fazer a uma API em um intervalo de tempo específico. Em um ecossistema digital onde APIs são a espinha dorsal de aplicações modernas, o rate limiting protege contra abusos intencionais (ataques DDoS, scraping) e não intencionais (bugs em clientes, loops de retry mal configurados). Sem ele, um único cliente mal comportado pode degradar a experiência de todos os outros usuários.

1.2. Equilíbrio entre segurança e experiência do usuário: evitar falsos positivos

O maior desafio do rate limiting não é proteger contra abusos óbvios, mas sim não penalizar usuários legítimos. Um limite muito restritivo pode bloquear uma integração válida durante um pico de uso sazonal. A chave está em implementar políticas que diferenciem comportamento anômalo de uso intenso legítimo.

1.3. Conceitos-chave: janela de tempo, limite de requisições, burst vs. throughput sustentado

Três conceitos fundamentais definem qualquer estratégia de rate limiting:
- Janela de tempo: o intervalo (ex: 1 minuto, 1 hora) durante o qual o limite é contado.
- Limite de requisições: o número máximo de chamadas permitidas na janela.
- Burst vs. throughput sustentado: um burst é um pico momentâneo de requisições; throughput sustentado é a média ao longo do tempo. Uma boa estratégia permite bursts controlados sem comprometer a estabilidade.

2. Algoritmos Clássicos de Rate Limiting

2.1. Token Bucket: flexibilidade para picos com consumo controlado

O algoritmo Token Bucket mantém um balde com capacidade máxima de tokens. A cada intervalo de tempo, tokens são adicionados até o limite. Cada requisição consome um token. Se o balde está vazio, a requisição é rejeitada. Isso permite bursts curtos (quando o balde está cheio) enquanto limita a taxa média.

# Exemplo conceitual de Token Bucket
CAPACIDADE_MAXIMA = 10
TOKENS_POR_SEGUNDO = 2
balde_atual = CAPACIDADE_MAXIMA

def verificar_requisicao():
    global balde_atual
    if balde_atual > 0:
        balde_atual -= 1
        return True
    else:
        return False

def recarregar_tokens():
    global balde_atual
    balde_atual = min(CAPACIDADE_MAXIMA, balde_atual + TOKENS_POR_SEGUNDO)

2.2. Leaky Bucket: suavização de tráfego e filas previsíveis

O Leaky Bucket funciona como um funil: as requisições entram em uma fila e são processadas a uma taxa constante. Se a fila enche, novas requisições são descartadas. É ideal para garantir vazão constante, mas menos flexível para bursts.

# Exemplo conceitual de Leaky Bucket
FILA_MAXIMA = 20
TAXA_PROCESSAMENTO = 5  # requisições por segundo
fila_atual = 0

def adicionar_requisicao():
    global fila_atual
    if fila_atual < FILA_MAXIMA:
        fila_atual += 1
        return True
    else:
        return False

def processar_fila():
    global fila_atual
    fila_atual = max(0, fila_atual - TAXA_PROCESSAMENTO)

2.3. Sliding Window Log vs. Sliding Window Counter: precisão versus eficiência de memória

  • Sliding Window Log: armazena um timestamp para cada requisição. Ao verificar, conta quantas requisições caem dentro da janela deslizante. Preciso, mas consome muita memória para APIs com alto throughput.
  • Sliding Window Counter: divide a janela em buckets menores (ex: 1 minuto dividido em 6 buckets de 10 segundos). Aproxima a contagem da janela deslizante com muito menos memória, aceitando pequena imprecisão.

3. Estratégias de Implementação no Backend

3.1. Rate limiting em memória (Redis, Memcached) com comandos atômicos (INCR, EXPIRE)

Redis é a escolha mais comum para rate limiting distribuído. O comando INCR incrementa um contador atômico, e EXPIRE define o TTL da janela.

# Exemplo com Redis (pseudo-código)
chave = "rate_limit:usuario123:minuto"
atual = redis.INCR(chave)
if atual == 1:
    redis.EXPIRE(chave, 60)  # expira em 60 segundos
if atual > 100:
    return HTTP 429 (Too Many Requests)
else:
    processar_requisicao()

3.2. Implementação em middleware de API Gateway (Kong, NGINX, Envoy)

Gateways como Kong e NGINX oferecem plugins nativos de rate limiting. No Kong, o plugin rate-limiting pode ser configurado por consumer, por rota ou globalmente, com suporte a Redis para sincronização entre nós.

# Configuração do plugin rate-limiting no Kong
plugins:
  - name: rate-limiting
    config:
      minute: 100
      hour: 5000
      policy: local  # ou "redis" para clusters

3.3. Rate limiting distribuído: consistência eventual e race conditions em clusters

Em clusters com múltiplos servidores, o rate limiting precisa de um armazenamento centralizado (Redis) ou de algoritmos de consenso. Race conditions podem ocorrer se duas requisições chegam simultaneamente e ambas verificam o contador antes de incrementá-lo. O uso de comandos atômicos (INCR, Lua scripts) resolve esse problema.

4. Políticas Inteligentes para Não Bloquear Usuários Legítimos

4.1. Diferenciação por endpoint: limites mais brandos para GETs, mais rígidos para POSTs/PUTs

Endpoints de leitura (GET) geralmente podem ter limites mais altos, enquanto endpoints de escrita (POST, PUT, DELETE) devem ser mais restritivos para evitar criação massiva de recursos ou alterações indevidas.

# Política de limites por método HTTP
GET /api/usuarios: 1000 req/min
POST /api/usuarios: 20 req/min
PUT /api/usuarios/:id: 10 req/min
DELETE /api/usuarios/:id: 5 req/min

4.2. Rate limiting baseado em perfil de usuário (free vs. premium, API key vs. OAuth)

Clientes com planos pagos ou autenticação mais forte (OAuth) podem receber limites maiores. A diferenciação pode ser feita pelo header Authorization ou pela chave de API.

# Limites por perfil
perfil = obter_perfil_do_usuario(token)
limites = {
    "free": {"minute": 10, "hour": 100},
    "premium": {"minute": 100, "hour": 1000},
    "enterprise": {"minute": 1000, "hour": 10000}
}
limite_atual = limites[perfil]

4.3. Técnicas de "soft limit": warnings, degradação gradual e filas de espera

Em vez de bloquear abruptamente, implemente soft limits:
- Warnings: headers indicando que o limite está próximo.
- Degradação gradual: aumentar o tempo de resposta em vez de rejeitar.
- Filas de espera: enfileirar requisições excedentes e processá-las quando houver capacidade.

5. Comunicação Clara com o Cliente: Headers e Respostas

5.1. Headers padrão: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset

Sempre inclua headers informativos nas respostas:

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1623456789

5.2. Respostas HTTP 429 (Too Many Requests) com Retry-After e corpo explicativo

Quando o limite é excedido, retorne 429 com o header Retry-After indicando quantos segundos o cliente deve esperar.

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

{
  "error": "rate_limit_exceeded",
  "message": "Limite de requisições excedido. Tente novamente em 30 segundos.",
  "retry_after_seconds": 30
}

5.3. Estratégias de backoff: exponencial, jitter e retry com idempotência

Clientes devem implementar backoff exponencial com jitter para evitar tempestades de retry. A idempotência dos endpoints ajuda a garantir que retries não causem efeitos colaterais.

# Exemplo de backoff exponencial com jitter
tentativa = 0
while tentativa < 5:
    resposta = fazer_requisicao()
    if resposta.status == 429:
        espera = (2 ** tentativa) + random.uniform(0, 1)
        time.sleep(espera)
        tentativa += 1
    else:
        break

6. Monitoramento, Alertas e Ajuste Fino

6.1. Métricas essenciais: taxa de rejeição, latência, distribuição de bursts

Monitore:
- Taxa de rejeição: % de requisições que retornam 429.
- Latência: aumento de latência em clientes próximos ao limite.
- Distribuição de bursts: horários e endpoints com maior concentração de requisições.

6.2. Logs de decisões de rate limiting para auditoria e debugging

Cada decisão de bloqueio deve ser registrada com timestamp, cliente, endpoint e motivo. Isso ajuda a identificar falsos positivos e padrões de abuso.

[2025-03-15 14:32:01] RATE_LIMIT_BLOCKED | cliente: api_key_abc123 | endpoint: POST /api/orders | motivo: limite_minuto_excedido (150/100)

6.3. Ajuste dinâmico de limites com base em padrões de tráfego (machine learning simples)

Sistemas avançados podem usar médias móveis ou modelos simples de ML para ajustar limites automaticamente. Por exemplo, aumentar temporariamente o limite para um cliente que sempre respeitou o rate limit durante um pico sazonal.

7. Considerações Avançadas e Armadilhas Comuns

7.1. Rate limiting em APIs GraphQL: consultas complexas e análise de profundidade

Em GraphQL, uma única requisição pode consultar múltiplos recursos. O rate limiting deve considerar a complexidade da query (profundidade, número de campos solicitados) em vez de apenas contar requisições.

# Atribuir peso a cada campo da query GraphQL
peso_query = calcular_peso(query_graphql)
if peso_query > 100:
    return HTTP 429

7.2. Sincronização entre microserviços: problemas de clock skew e consistência

Em ambientes distribuídos, clocks de servidores podem divergir. Use Redis ou um serviço de clock global para evitar inconsistências. Evite depender de timestamps locais para decisões de rate limiting.

7.3. Testes de carga e simulação de cenários de abuso vs. uso legítimo intenso

Antes de colocar em produção, simule:
- Cenário de abuso: requisições em rajada de um único IP.
- Cenário legítimo intenso: múltiplos usuários fazendo requisições simultâneas durante uma campanha de marketing.
Ajuste os limites com base nos resultados para evitar falsos positivos.

Referências