Um cliente travado num retry loop sem backoff pode colocar mais carga na sua API em um minuto do que os outros dez mil clientes juntos. Do seu lado, isso não parece um ataque. Parece um bug do lado deles, e se torna um incidente do seu lado. Um rate limiter contém esse cliente antes que ele chegue a esse ponto.

O que um rate limiter realmente decide?

A cada request, o limiter se faz uma única pergunta: essa chave fez requests demais na janela atual? Três escolhas transformam isso em algo que você pode implementar.

A chave é aquilo contra o qual você conta: um endereço IP, uma API key, o ID de um usuário autenticado. Escolha a errada e você acaba bloqueando usuários inocentes que compartilham uma identidade, ou deixando o verdadeiro abusador se esconder entre eles.

A janela e o limite só fazem sentido juntos. “100 requests” não é um limite; 100 por segundo e 100 por dia são duas APIs muito diferentes.

A ação é o que acontece quando a chave está acima do limite. A maioria das APIs rejeita com 429 Too Many Requests. Um job em segundo plano ou quem envia webhooks tende a colocar o trabalho numa fila e drená-lo depois, que é a função de uma message queue (veja O Que É uma Fila de Mensagens e Como Usar).

Qual algoritmo, qual store, quais headers: tudo isso é implementação em volta dessa única verificação.

Quatro algoritmos de rate limiting e o que cada um erra

Fixed window counter. Incrementa um contador para a chave e zera quando o relógio cruza um limite, digamos a cada minuto exato. Custa um inteiro por chave. A falha: um cliente que manda 100 requests às 0:59 e outras 100 à 1:00 enviou 200 requests em dois segundos sem violar “100 por minuto” em nenhuma das duas janelas. No limite, uma fixed window deixa passar até o dobro da taxa declarada.

Sliding window log. Guarda o timestamp de cada request para a chave (um sorted set do Redis serve bem), e a cada nova request conta quantas caem nos últimos 60 segundos. É exato, então o truque do limite não funciona. O custo é uma memória que cresce com o tráfego: contar um milhão de timestamps por chave não é o mesmo que ler um inteiro. É a mesma diferença entre percorrer uma lista e ler um índice — a diferença que Notação Big O Explicada com Exemplos de Código nomeia.

Sliding window counter. É o que a maioria das APIs deveria usar. Mantenha dois contadores de janela fixa, o atual e o anterior, e estime a contagem deslizante como contagem_atual + contagem_anterior * (fração de sobreposição). Se você está em 40% do minuto atual, conta todas as requests da janela atual mais 60% da anterior. Dois inteiros por chave, nenhum exploit de limite, e próximo o suficiente do exato para limites reais.

Token bucket. Um bucket contém até capacity tokens e se recarrega a uma rate fixa por segundo. Cada request consome um token; sem token, nenhuma request. Um cliente pode fazer um burst de até capacity requests de uma vez, e depois fica limitado à taxa de recarga. Isso corresponde a como navegadores e apps realmente chamam APIs: o carregamento de uma página dispara oito requests juntas, depois nada por um tempo. Um teto fixo por segundo rejeitaria metade desse carregamento de página.

Leaky bucket. Mesmo bucket, objetivo oposto. As requests que chegam entram numa fila e saem a uma taxa constante, então os bursts se transformam num fluxo estável. O token bucket decide quanto burst é permitido a um cliente. O leaky bucket decide quão constante é a taxa de saída. Use-o quando o que está a jusante, uma API de terceiros com sua própria quota ou um worker pool de tamanho fixo, precisa de um fluxo constante.

Como construir um rate limiter com Redis

O contador precisa viver num lugar que cada instância da sua API consiga ver. Um contador em memória do processo funciona até você rodar uma segunda instância; depois disso, cada uma aplica o limite sobre apenas metade do tráfego. Redis é o store de uso comum (veja Como Funciona o Cache Redis e Como Usá-lo se você ainda não configurou um), e o motivo são os comandos atômicos dele mais do que a velocidade.

Um fixed window counter em Python com INCR e EXPIRE:

1
2
3
4
5
def is_allowed(redis, key: str, limit: int, window_seconds: int) -> bool:
    count = redis.incr(f"rl:{key}")
    if count == 1:
        redis.expire(f"rl:{key}", window_seconds)
    return count <= limit

INCR é atômico numa única instância Redis, então duas requests que chegam no mesmo microssegundo ainda recebem duas contagens distintas. O bug que a maioria dos limiters feitos à mão carrega é um GET seguido de um SET. Duas requests concorrentes leem a mesma contagem, as duas decidem que estão abaixo do limite, e as duas passam. Resta uma lacuna na versão acima: se seu processo morre entre INCR e EXPIRE, a chave nunca expira e esse cliente fica bloqueado. Mover as duas chamadas para um script Lua, como o token bucket abaixo, fecha essa lacuna.

O token bucket precisa de três passos: calcular o refill, tomar um token, escrever o estado de volta. Nenhum comando Redis faz os três sozinho, então você os executa como um script Lua via EVAL, que o Redis executa como uma única unidade atômica:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- KEYS[1] = bucket key, ARGV[1] = capacity, ARGV[2] = refill rate/sec, ARGV[3] = now
local bucket = redis.call("HMGET", KEYS[1], "tokens", "ts")
local tokens = tonumber(bucket[1]) or tonumber(ARGV[1])
local last = tonumber(bucket[2]) or tonumber(ARGV[3])
local elapsed = tonumber(ARGV[3]) - last
tokens = math.min(tonumber(ARGV[1]), tokens + elapsed * tonumber(ARGV[2]))
local ttl = math.ceil(tonumber(ARGV[1]) / tonumber(ARGV[2]))

if tokens < 1 then
  redis.call("HSET", KEYS[1], "tokens", tokens, "ts", ARGV[3])
  redis.call("EXPIRE", KEYS[1], ttl)
  return 0
end

redis.call("HSET", KEYS[1], "tokens", tokens - 1, "ts", ARGV[3])
redis.call("EXPIRE", KEYS[1], ttl)
return 1

Chame-o com redis.eval(script, 1, f"bucket:{key}", capacity, refill_rate, time.time()). O script recarrega o bucket pelo tempo que passou, toma um token na mesma passagem, e define um TTL longo o bastante para um refill completo, assim um cliente que para de chamar a API não deixa seu bucket ocupando espaço no Redis para sempre. Nenhuma outra request pode ler ou alterar o bucket enquanto ele está em execução.

Quais headers enviar com uma resposta 429

Uma request rejeitada deveria dizer ao cliente quando voltar a tentar.

Use 429 Too Many Requests, não 403. O cliente não está proibido, está adiantado.

Adicione Retry-After: 30, em segundos ou como data HTTP. Muitas bibliotecas cliente e SDKs HTTP o leem para agendar sua retentativa, e as que não leem pelo menos dão a uma pessoa algo para se orientar.

Envie X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset em toda resposta, não só nas rejeitadas. Eles dizem ao cliente o teto, a quota restante e quando a janela reinicia. É uma convenção de fato (a API do GitHub a usa) mais do que um padrão, mas é o que quem desenvolve clientes procura. Um cliente bem-comportado observa Remaining cair e desacelera antes mesmo de ver um 429.

Existe uma versão padrão em rascunho: os campos RateLimit e RateLimit-Policy do draft-ietf-httpapi-ratelimit-headers do IETF. É um Internet-Draft há vários anos e ainda não é um RFC. Envie os headers X-RateLimit-* agora, e adicione os campos padrão quando o rascunho for publicado como RFC.

Erros comuns de rate limiting

Limitar só por IP. Uma rede corporativa, o NAT de uma operadora móvel ou um edge de CDN podem colocar milhares de usuários reais atrás de um único endereço. Use como chave a API key ou o ID do usuário autenticado onde você tiver um, e recorra ao IP só para tráfego anônimo. JWT vs Sessão: Qual Autenticação Escolher explica como essa identidade é estabelecida.

Um contador em memória atrás de um load balancer. Cada instância conta por conta própria, então um limite de 100 se torna 100 × o número de instâncias. Assim que você escala horizontalmente, o contador precisa viver no Redis, não num dict local ao processo.

Um único limite para toda a API. Uma busca e uma exportação em massa não custam o mesmo para servir. Defina limites por rota, ou pelo menos por classe de custo.

Zero tolerância a burst. Uma retentativa de webhook, uma importação em massa, um app mobile se reconectando depois de um túnel: para um teto fixo por segundo, tudo isso parece abuso. O token bucket existe exatamente para esse tráfego.

Qual rate limiter construir primeiro?

Para uma API típica com muita leitura, construa um sliding window counter com chave na API key, armazenado no Redis, com os headers X-RateLimit-* em toda resposta. Isso evita o burst no limite da fixed window e o custo de memória de um log completo de requests. Adicione um token bucket só nas rotas em que clientes legitimamente enviam lotes (entrega de webhooks, uploads em massa), e defina sua capacity pelo tamanho do maior lote esperado, não pela taxa em regime estável.

Artigos relacionados