Rate limiting é o mecanismo pelo qual um sistema controla a taxa de requisições que aceita de um cliente ou de um conjunto de clientes. Sem rate limiting, um único cliente mal-comportado — intencional ou acidentalmente — pode consumir todos os recursos disponíveis e derrubar o serviço para todos os outros. Com rate limiting mal projetado, clientes legítimos são bloqueados desnecessariamente e a API se torna não confiável. A diferença está nos algoritmos escolhidos, na granularidade da identidade do cliente, e em como o sistema comunica os limites.
Rate limiting não é apenas proteção contra ataques — é também um mecanismo de modelagem de tráfego (traffic shaping) para garantir que a capacidade do sistema seja distribuída de forma previsível entre os clientes, e um sinal de qualidade de API para clientes legítimos que precisam saber com antecedência o que esperar.
Os quatro algoritmos principais
Fixed Window Counter
O algoritmo mais simples: conta as requisições em janelas de tempo fixas (ex: 1 minuto). Quando a contagem ultrapassa o limite, requisições adicionais são rejeitadas até a próxima janela começar.
-- Redis: contador por janela fixa
-- Chave: "ratelimit:{client_id}:{window_start}"
-- window_start = floor(now / window_size) * window_size
local key = "ratelimit:" .. client_id .. ":" .. window_start
local count = redis.call("INCR", key)
if count == 1 then
redis.call("EXPIRE", key, window_size_seconds)
end
return count <= limit -- true = permitido
Problema crítico do fixed window: boundary burst. Um cliente pode enviar 100 requisições no final de uma janela e mais 100 no início da próxima — 200 requisições em poucos segundos, mesmo com limite de 100/minuto. Esse comportamento é determinístico e exploitável.
Janela 1: [... ... ... 100 req nos últimos 2s]
Janela 2: [100 req nos primeiros 2s ...]
Total: 200 req em 4 segundos — limite violado na prática
Sliding Window Log
Mantém um log de timestamps de cada requisição. Para verificar o limite, conta quantas requisições ocorreram nos últimos N segundos (janela deslizante). Elimina o boundary burst — a janela é sempre relativa ao momento atual.
-- Redis Sorted Set: timestamps como score, request_id como member
-- Permite contar eficientemente quantas requisições ocorreram em [now-window, now]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2]) -- ex: 60000 ms
local limit = tonumber(ARGV[3])
local key = KEYS[1]
-- Remove timestamps fora da janela
redis.call("ZREMRANGEBYSCORE", key, 0, now - window)
-- Conta requisições na janela atual
local count = redis.call("ZCARD", key)
if count < limit then
-- Adiciona a requisição atual
redis.call("ZADD", key, now, now .. "-" .. math.random())
redis.call("EXPIRE", key, math.ceil(window / 1000) + 1)
return 1 -- permitido
end
return 0 -- bloqueado
Precisão perfeita, mas o armazenamento cresce proporcionalmente ao número de requisições — para limites altos (ex: 10.000/hora), o sorted set fica grande. Adequado para limites baixos ou quando a precisão é crítica (ex: APIs de pagamento).
Sliding Window Counter
Compromisso entre fixed window e sliding window log: mantém contadores de duas janelas fixas adjacentes e calcula o valor interpolado, estimando o rate da janela deslizante sem armazenar timestamps individuais.
-- Fórmula de aproximação
-- current_window_count: requisições na janela atual
-- previous_window_count: requisições na janela anterior
-- elapsed: fração da janela atual já transcorrida (0.0 a 1.0)
estimated_count = previous_window_count * (1 - elapsed) + current_window_count
-- Exemplo:
-- Janela anterior: 80 req
-- Janela atual: 30 req
-- elapsed: 0.4 (40% da janela atual passou)
-- estimated: 80 * 0.6 + 30 = 78 req estimados na janela deslizante
A aproximação tem erro máximo de ~0.003% em distribuições uniformes — na prática, imperceptível. É o algoritmo usado pelo Cloudflare em seus sistemas de rate limiting distribuído. Armazenamento constante (dois inteiros por cliente por janela), sem crescimento proporcional ao tráfego.
Token Bucket
Um balde contém tokens. Cada requisição consome um token. Tokens são adicionados a uma taxa constante (ex: 10 tokens/segundo). Se o balde está vazio, a requisição é rejeitada. O balde tem capacidade máxima — tokens não acumulam além do limite.
-- Token Bucket com Redis (implementação atômica via Lua)
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- capacidade máxima do balde
local rate = tonumber(ARGV[2]) -- tokens adicionados por segundo
local now = tonumber(ARGV[3]) -- timestamp atual em ms
local requested = tonumber(ARGV[4]) -- tokens necessários (geralmente 1)
local data = redis.call("HMGET", key, "tokens", "last_refill")
local tokens = tonumber(data[1]) or capacity
local last_refill = tonumber(data[2]) or now
-- Calcular tokens adicionados desde o último acesso
local elapsed = (now - last_refill) / 1000 -- em segundos
local new_tokens = math.min(capacity, tokens + elapsed * rate)
if new_tokens >= requested then
-- Permitido: consumir tokens
redis.call("HMSET", key, "tokens", new_tokens - requested, "last_refill", now)
redis.call("EXPIRE", key, math.ceil(capacity / rate) + 10)
return {1, math.floor(new_tokens - requested)} -- {permitido, tokens_restantes}
else
redis.call("HMSET", key, "tokens", new_tokens, "last_refill", now)
return {0, math.floor(new_tokens)} -- {bloqueado, tokens_disponíveis}
end
O token bucket permite bursts controlados: se o cliente ficou inativo, acumula tokens até o limite de capacidade e pode usá-los em burst. É o algoritmo ideal quando você quer suavizar picos mas permitir aceleração temporária legítima (ex: uma pipeline que processa um lote grande ocasionalmente).
Leaky Bucket
Requisições entram em uma fila (o balde) e saem a uma taxa constante. Se o balde transborda (fila cheia), a requisição é rejeitada. Garante saída uniforme — não importa como as requisições chegam, o sistema downstream as recebe sempre na mesma taxa.
-- Leaky Bucket: implementado como fila com rate de dequeue fixo
-- Menos comum em APIs HTTP; mais usado em traffic shaping de rede
-- Em APIs, token bucket é preferido por ser mais simples e igualmente efetivo
-- Diferença fundamental:
-- Token bucket: permite burst até a capacidade, suaviza a média
-- Leaky bucket: garante taxa constante de saída, sem burst downstream
Dimensões de identidade do cliente
Rate limiting por IP é o mínimo, mas é insuficiente em produção. Múltiplos clientes podem compartilhar um IP (NAT corporativo, VPN), e atacantes usam proxies. As dimensões mais úteis:
| Dimensão | Chave Redis | Quando usar |
|---|---|---|
| IP | rl:ip:{ip} |
Proteção básica, anonymous APIs |
| API Key | rl:key:{api_key} |
APIs autenticadas, limites por plano |
| User ID | rl:user:{user_id} |
Usuários autenticados, independente de IP |
| Tenant | rl:tenant:{tenant_id} |
SaaS multi-tenant, isolamento por cliente |
| Endpoint | rl:{dim}:{id}:{path} |
Limites diferentes por endpoint custoso |
| Global | rl:global:{service} |
Proteção do sistema inteiro independente do cliente |
Em produção, aplique múltiplos limites em camadas: limite global por IP (proteção contra ataques de força bruta), limite por API key ou usuário (fair use), e limite por endpoint para operações custosas (ex: busca full-text = 10/min, leitura de item = 1000/min).
Headers de resposta — RFC 6585 e convenções
Clientes precisam saber o estado do rate limiting para implementar backoff correto. Os headers padrão:
HTTP/1.1 200 OK
RateLimit-Limit: 1000 # limite total na janela
RateLimit-Remaining: 876 # requisições restantes na janela atual
RateLimit-Reset: 1715299200 # Unix timestamp quando a janela reseta
Retry-After: 45 # segundos a aguardar (em respostas 429)
# Quando bloqueado:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
RateLimit-Limit: 1000
RateLimit-Remaining: 0
RateLimit-Reset: 1715299200
Retry-After: 45
{
"error": "rate_limit_exceeded",
"message": "Limite de 1000 req/hora atingido",
"retry_after": 45
}
A IETF padronizou o formato em RFC 9110 e draft-ietf-httpapi-ratelimit-headers. Evite headers proprietários como X-RateLimit-* em APIs novas — os headers sem prefixo X- são o padrão atual. Inclua Retry-After em todas as respostas 429 — sem ele, clientes fazem backoff arbitrário ou continuam tentando.
Rate limiting distribuído
Em um sistema com múltiplas instâncias do serviço, o rate limiting local (em memória) conta apenas as requisições que chegam a uma instância específica — clientes podem bypass rotando entre instâncias. Rate limiting distribuído centraliza o estado no Redis.
O trade-off é latência: cada verificação de rate limit requer uma round-trip ao Redis. Em endpoints de alta frequência, isso pode ser significativo. Otimizações:
Script Lua no Redis: execute a lógica de verificação + incremento atomicamente no Redis via script Lua — evita race conditions e reduz o número de round-trips para um único comando.
Cache local com TTL curto: mantenha um cache local do estado do rate limit por 100-500ms. Cada instância confirma com Redis periodicamente, não em cada requisição. Aceita pequeno overshoot (cliente pode exceder em ~tráfego de 500ms) em troca de latência de verificação de microsegundos.
Redis Cluster: para sistemas com milhões de clientes simultâneos, distribua as chaves de rate limit em um cluster Redis usando hash slots. Cada chave vai para um nó específico — sem coordenação entre nós para a operação de rate limit.
Throttling vs rate limiting
Rate limiting rejeita requisições que excedem o limite com 429. O cliente deve esperar e tentar novamente. Throttling (ou traffic shaping) atrasa requisições em vez de rejeitar — enfileira e processa mais devagar. Throttling preserva mais requisições mas pode causar timeouts se a fila crescer além da capacidade.
Em APIs HTTP públicas, rate limiting (rejeição com 429) é o padrão — dá feedback imediato ao cliente e não consome memória com filas longas. Throttling é mais comum em sistemas internos ou pipelines de processamento onde descartar trabalho é inaceitável.
Comparativo entre linguagens — middleware de rate limiting
// C# — Rate limiting com ASP.NET Core RateLimiter (built-in, .NET 7+)
// e Redis para estado distribuído via AspNetCoreRateLimit
// Program.cs — configuração built-in (in-process, sem Redis)
builder.Services.AddRateLimiter(options => {
// Limite global por IP — sliding window
options.AddPolicy("per-ip", httpContext =>
RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
factory: _ => new SlidingWindowRateLimiterOptions {
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6, // janela dividida em 6 segmentos de 10s
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0, // sem fila — rejeição imediata
}
)
);
// Limite por API key — token bucket
options.AddPolicy("per-apikey", httpContext => {
var apiKey = httpContext.Request.Headers["X-API-Key"].ToString();
return string.IsNullOrEmpty(apiKey)
? RateLimitPartition.GetNoLimiter("anonymous")
: RateLimitPartition.GetTokenBucketLimiter(
partitionKey: $"apikey:{apiKey}",
factory: _ => new TokenBucketRateLimiterOptions {
TokenLimit = 1000,
ReplenishmentPeriod = TimeSpan.FromHours(1),
TokensPerPeriod = 1000,
AutoReplenishment = true,
}
);
});
// Resposta 429 customizada com headers corretos
options.OnRejected = async (context, token) => {
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) {
context.HttpContext.Response.Headers["Retry-After"] =
((int)retryAfter.TotalSeconds).ToString();
}
context.HttpContext.Response.Headers["RateLimit-Limit"] = "100";
await context.HttpContext.Response.WriteAsJsonAsync(new {
error = "rate_limit_exceeded",
retry_after = (int?)retryAfter.TotalSeconds
}, token);
};
});
// Aplicação por endpoint
app.MapGet("/api/search", SearchHandler)
.RequireRateLimiting("per-apikey");
app.MapPost("/api/orders", OrderHandler)
.RequireRateLimiting("per-ip");
O RateLimiter built-in do ASP.NET Core é in-process — não compartilha estado entre instâncias. Para ambientes com múltiplas réplicas, use AspNetCoreRateLimit com Redis backend ou implemente via middleware customizado com StackExchange.Redis.
# Python — Rate limiting com FastAPI e Redis (token bucket via Lua script)
import time
import hashlib
from typing import Callable
from fastapi import FastAPI, Request, Response, HTTPException
from fastapi.routing import APIRoute
import redis.asyncio as aioredis
app = FastAPI()
redis_client: aioredis.Redis = None
# Script Lua para token bucket — executado atomicamente no Redis
TOKEN_BUCKET_SCRIPT = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(data[1]) or capacity
local last_refill = tonumber(data[2]) or now
local elapsed = (now - last_refill) / 1000
local new_tokens = math.min(capacity, tokens + elapsed * rate)
if new_tokens >= 1 then
redis.call('HMSET', key, 'tokens', new_tokens - 1, 'last_refill', now)
redis.call('EXPIRE', key, math.ceil(capacity / rate) + 10)
return {1, math.floor(new_tokens - 1), math.ceil((1 - new_tokens + 1) / rate * 1000)}
else
redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
local wait_ms = math.ceil((1 - new_tokens) / rate * 1000)
return {0, 0, wait_ms}
end
"""
def rate_limit(capacity: int = 100, rate: float = 100/3600):
"""Decorator de rate limit por IP usando token bucket."""
script = None
def decorator(func: Callable):
async def wrapper(request: Request, *args, **kwargs):
nonlocal script
if script is None:
script = redis_client.register_script(TOKEN_BUCKET_SCRIPT)
client_ip = request.client.host
key = f"rl:ip:{client_ip}"
now_ms = int(time.time() * 1000)
allowed, remaining, wait_ms = await script(
keys=[key],
args=[capacity, rate, now_ms]
)
# Adiciona headers de rate limit sempre
headers = {
"RateLimit-Limit": str(capacity),
"RateLimit-Remaining": str(remaining),
}
if not allowed:
retry_after = max(1, wait_ms // 1000)
raise HTTPException(
status_code=429,
detail={
"error": "rate_limit_exceeded",
"retry_after": retry_after,
},
headers={**headers, "Retry-After": str(retry_after)},
)
response = await func(request, *args, **kwargs)
for k, v in headers.items():
response.headers[k] = v
return response
return wrapper
return decorator
@app.get("/api/search")
@rate_limit(capacity=10, rate=10/60) # 10 req/minuto
async def search(request: Request, q: str):
return {"results": [], "query": q}
O script Lua executado via register_script garante atomicidade — sem race condition entre verificar e decrementar o contador. O rate é em tokens por milissegundo para consistência com o timestamp em ms.
// Go — middleware de rate limiting com golang.org/x/time/rate (in-process)
// e Redis para distribuído
package middleware
import (
"context"
"encoding/json"
"net/http"
"strconv"
"sync"
"time"
"golang.org/x/time/rate"
"github.com/redis/go-redis/v9"
)
// In-process: limiter por IP com cleanup de entradas inativas
type IPRateLimiter struct {
limiters sync.Map
rate rate.Limit // tokens por segundo
burst int // capacidade máxima
}
func NewIPRateLimiter(r rate.Limit, burst int) *IPRateLimiter {
l := &IPRateLimiter{rate: r, burst: burst}
go l.cleanup() // goroutine de limpeza de limiters inativos
return l
}
func (l *IPRateLimiter) getLimiter(ip string) *rate.Limiter {
v, _ := l.limiters.LoadOrStore(ip, rate.NewLimiter(l.rate, l.burst))
return v.(*rate.Limiter)
}
func (l *IPRateLimiter) cleanup() {
ticker := time.NewTicker(10 * time.Minute)
for range ticker.C {
l.limiters.Range(func(k, _ any) bool {
l.limiters.Delete(k)
return true
})
}
}
func (l *IPRateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
limiter := l.getLimiter(ip)
reservation := limiter.Reserve()
if !reservation.OK() {
w.Header().Set("RateLimit-Limit", strconv.Itoa(l.burst))
w.Header().Set("RateLimit-Remaining", "0")
w.Header().Set("Retry-After", "1")
w.WriteHeader(http.StatusTooManyRequests)
json.NewEncoder(w).Encode(map[string]any{
"error": "rate_limit_exceeded",
"retry_after": 1,
})
return
}
delay := reservation.Delay()
if delay > 100*time.Millisecond {
// Delay muito longo — cancelar e rejeitar
reservation.Cancel()
retryAfter := int(delay.Seconds()) + 1
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
w.WriteHeader(http.StatusTooManyRequests)
json.NewEncoder(w).Encode(map[string]any{
"error": "rate_limit_exceeded",
"retry_after": retryAfter,
})
return
}
// Pequeno delay aceitável — throttle ao invés de rejeitar
if delay > 0 {
time.Sleep(delay)
}
w.Header().Set("RateLimit-Limit", strconv.Itoa(l.burst))
next.ServeHTTP(w, r)
})
}
// Distributed: Redis token bucket via Lua script
var tokenBucketScript = redis.NewScript(`
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(data[1]) or capacity
local ts = tonumber(data[2]) or now
local new_tokens = math.min(capacity, tokens + (now - ts) * rate / 1000)
if new_tokens >= 1 then
redis.call('HMSET', key, 'tokens', new_tokens - 1, 'ts', now)
redis.call('EXPIRE', key, math.ceil(capacity / rate) + 5)
return 1
end
redis.call('HMSET', key, 'tokens', new_tokens, 'ts', now)
return 0
`)
func RedisRateLimit(rdb *redis.Client, capacity int, ratePerSec float64) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := "rl:ip:" + r.RemoteAddr
nowMs := time.Now().UnixMilli()
allowed, err := tokenBucketScript.Run(r.Context(), rdb,
[]string{key}, capacity, ratePerSec, nowMs,
).Int()
if err != nil {
// Fail-open: Redis indisponível → permitir requisição
next.ServeHTTP(w, r)
return
}
if allowed == 0 {
w.Header().Set("Retry-After", "1")
w.WriteHeader(http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
golang.org/x/time/rate implementa token bucket in-process com API Reserve() que permite cancelar reservas não usadas. Para estado distribuído, o script Lua no Redis garante atomicidade. O fail-open em erro de Redis é a política correta para APIs de produção.
Decisões de engenharia
Fixed Window: simples e barato (1 chave Redis com TTL). Adequado quando boundary burst não é uma ameaça real — por exemplo, rate limiting interno entre microsserviços onde o tráfego é previsível. Evite para APIs públicas onde clientes podem explorar a janela.
Sliding Window Counter: elimina o boundary burst com custo marginal (2 chaves, interpolação linear). É o melhor custo-benefício para a maioria das APIs públicas. A aproximação de 10% de erro no pior caso é aceitável em produção.
Token Bucket: permite burst controlado (o bucket acumula tokens quando ocioso). Adequado quando você quer permitir rajadas legítimas — por exemplo, um serviço de background que faz batch de requisições. O leaky bucket é o inverso: suaviza o tráfego de saída em vez de controlar o de entrada. Token bucket é padrão em SDKs de AWS, Stripe e GCP.
Por IP: proteção base contra abuso sem autenticação. Limitado: IPs são facilmente spoofados ou compartilhados (NAT corporativo, proxies). Útil como primeira camada defensiva, especialmente para endpoints não autenticados como login e registro.
Por API key: rastreia clientes identificados (empresas, aplicações). Permite limites diferenciados por tier (free vs paid). Pode ser combinado com por IP para detectar chaves compartilhadas. É o padrão para APIs B2B.
Por usuário autenticado: mais granular que por IP, mais preciso que por API key para APIs consumer. Combine os três em camadas: 10/min por IP (anônimo) + 1000/hora por API key + 100/min por usuário + 10/min para endpoints específicos. Cada camada protege contra um tipo diferente de abuso.
In-process: zero latência, zero overhead de rede. Adequado quando cada instância pode ter seu limite independente — por exemplo, 100 req/s por instância de um serviço com 10 instâncias (limite efetivo: 1000 req/s total). Falha quando instâncias são criadas e destruídas dinamicamente (o estado é perdido) e quando o limite global precisa ser preciso.
Distribuído (Redis + Lua): estado centralizado e compartilhado entre todas as instâncias. Essencial quando o limite é global e preciso — "máximo 1000 req/min para esta API key independente de quantas instâncias do gateway estão rodando". O custo é a latência do Redis (~1ms no P99) em cada requisição. Use pipeline ou scripts Lua para atomicidade. Defina fail-open em falha do Redis — o rate limiter não deve ser um SPOF da sua API.
Rate Limiting: proteção do servidor contra o cliente. O servidor define o teto de requisições aceitáveis. É uma política do servidor aplicada ao cliente. Mecanismo: rejeitar com 429 quando o cliente excede o limite.
Circuit Breaker: proteção do cliente contra o servidor. O cliente detecta que o servidor está falhando e para de enviar requisições temporariamente. É uma política do cliente aplicada a chamadas de saída. Mecanismo: abrir o circuito após N falhas consecutivas, tentativa de fechamento após timeout.
Throttling: modelagem de tráfego — em vez de rejeitar, o sistema atrasa requisições para suavizar a taxa. O cliente não recebe 429; recebe a resposta tarde. Adequado quando o custo de rejeição (forçar retry no cliente) é maior que o custo de enfileirar. Implementado com filas de espera ou backpressure no producer.
-
Demonstre o boundary burst do fixed window: implemente um contador simples e escreva um script que envia 100 requisições nos últimos 2 segundos de uma janela e mais 100 nos primeiros 2 segundos da próxima. Meça quantas passam. Em seguida, substitua por sliding window counter com interpolação linear e repita o experimento.
Critério: com fixed window: até 200 requisições passam em 4s; com sliding window: no máximo 100 + margem de interpolação passam; diferença documentada com números reais. -
Implemente token bucket com Redis usando script Lua. Valide atomicidade: inicie 20 goroutines/threads simultâneas, todas tentando consumir o último token ao mesmo tempo. Verifique que apenas uma consegue e as outras recebem 429. Adicione logging para mostrar a ordem de execução.
Critério: com 1 token restante e 20 goroutines simultâneas, exatamente 1 é aceita e 19 recebem 429; nenhuma condição de corrida detectada em 1000 repetições do teste. -
Configure rate limiting em camadas para uma API FastAPI ou ASP.NET Core: 1000/hora por API key (Redis), 100/minuto por IP (in-process), e 10/minuto para o endpoint
POST /ordersespecificamente. Implemente os headersRateLimit-Limit,RateLimit-RemainingeRetry-Afterem todas as respostas.
Critério: os três limites funcionam independentemente; ao exceder qualquer um, o response inclui headers corretos com valores precisos; testes automatizados verificam cada camada isoladamente. -
Implemente o retry client correto: um cliente que lê o header
Retry-Afterde respostas 429 e aguarda exatamente o tempo indicado antes de tentar novamente, com jitter de ±10%. Com 20 clientes simultâneos todos recebendo 429 ao mesmo tempo, demonstre que o jitter distribui os retries ao longo do tempo em vez de criar thundering herd.
Critério: histograma dos tempos de retry mostra distribuição uniforme de ±10% em torno do Retry-After; sem jitter, todos os 20 clientes retentam exatamente no mesmo segundo. -
Meça o overhead de latência do rate limiting distribuído vs in-process: com um benchmark de 10.000 req/s usando
k6ouwrk, compare o P50, P95 e P99 de latência de uma rota com rate limit in-process (token bucket em memória) vs distribuído (Redis Lua script). Identifique quando o overhead do Redis se torna significativo.
Critério: benchmark com resultados tabulados para P50/P95/P99; análise do ponto de inflexão onde latência do Redis impacta P99 significativamente; recomendação de quando usar cada abordagem baseada nos dados.
Perguntas de entrevista
Explique o boundary burst problem do Fixed Window Counter e como algoritmos mais sofisticados resolvem isso.
O problema: o Fixed Window divide o tempo em janelas fixas (ex: 1 minuto). Um cliente com limite de 100 req/min pode enviar 100 requisições às 00:59 e outras 100 às 01:00 — 200 requisições em 2 segundos, sem violar o limite tecnicamente. Esse "boundary burst" é determinístico e exploitável.
Sliding Window Log: mantém o timestamp de cada requisição. Para cada nova requisição, remove timestamps mais antigos que a janela (ex: > 1 minuto atrás) e conta os restantes. Resolve o boundary burst com precisão perfeita, mas o custo de memória é proporcional ao número de requisições no histórico — inviável para alta volumetria.
Sliding Window Counter: combina dois contadores de janelas fixas com interpolação linear. A contagem atual = contador_janela_atual + contador_janela_anterior × (fração do tempo restante na janela anterior). Ex: com 70% do tempo na janela atual decorrido, conta 30% das requisições da janela anterior. Resolve boundary burst com erro máximo de ~10% e custo O(1) — é o algoritmo mais prático para produção.
Token Bucket: o bucket acumula tokens ao longo do tempo (taxa de recarga) e perde um token por requisição. O burst é controlado pela capacidade máxima do bucket. Não é uma "sliding window" mas resolve boundary burst de forma diferente — o burst é permitido mas limitado ao tamanho do bucket.
Como implementar rate limiting distribuído de forma atômica? O que pode dar errado sem atomicidade?
O problema de atomicidade: sem atomicidade, duas instâncias lendo o contador simultaneamente podem ambas ver "99 de 100" e ambas permitir a requisição — resultando em 101 requisições passando. Em Redis, uma operação não atômica seria: GET contador → verificar → INCR → SET TTL. Entre o GET e o INCR, outra instância pode executar a mesma sequência.
Solução com INCR + EXPIRE: INCR retorna o valor após incremento atomicamente. EXPIRE define o TTL apenas se for a primeira requisição (count == 1). É atômico para a contagem mas não para o EXPIRE — se o processo morrer entre INCR e EXPIRE, o contador persiste para sempre. Mitigação: sempre definir TTL após INCR independentemente.
Solução com Lua script: Redis executa scripts Lua atomicamente (nenhuma outra operação pode intercalar). O script pode ler, verificar, incrementar e expirar em uma única operação atômica garantida. É a abordagem recomendada para algoritmos complexos (sliding window, token bucket).
Solução com Redis Transactions (MULTI/EXEC): permite executar múltiplos comandos atomicamente, mas não permite condicional baseada em valor intermediário — você não pode verificar o contador dentro da transação. Útil para decrement + check em set de tokens.
Qual a diferença entre rate limiting, circuit breaker e throttling? Quando você usa cada um?
Rate Limiting: "o servidor protege a si mesmo do cliente". O servidor define um teto de requisições por janela de tempo para cada cliente/IP/API key. Quando excedido, retorna 429 Too Many Requests. É unilateral e estático — o limite é configurado por política, não pela saúde do sistema.
Circuit Breaker: "o cliente protege a si mesmo do servidor". O cliente monitora a taxa de falhas em chamadas a um serviço downstream. Quando a taxa excede um threshold (ex: >50% de falhas em 10s), o circuito abre e requisições subsequentes falham fast sem sequer tentar o downstream. Após um timeout, o circuito vai para half-open (uma requisição de teste) e fecha se bem-sucedida. Previne cascata de falhas e dá tempo ao downstream de se recuperar.
Throttling: "o sistema desacelera em vez de rejeitar". Em vez de 429, o servidor coloca a requisição em fila e processa quando possível. O cliente espera mais, mas a requisição não falha. É adequado quando o cliente não tem lógica de retry e quando perder a requisição tem custo alto. Implementado com backpressure: producers são pausados quando a fila enche.
Quando usar: rate limiting para proteção de API pública por quota. Circuit breaker em cada chamada para serviço downstream crítico. Throttling quando clientes legítimos fazem burst e você prefere atrasar a rejeitar.
Quais headers HTTP um servidor deve retornar para rate limiting e como clientes deveriam usá-los?
Headers padrão (IETF draft em padronização):
- RateLimit-Limit: o limite configurado (ex: 100 ou 100, 100;w=60 para indicar 100 por 60s)
- RateLimit-Remaining: quantas requisições restam na janela atual
- RateLimit-Reset: timestamp Unix ou segundos até a janela resetar
- Retry-After: obrigatório em 429, indica o tempo mínimo antes do próximo retry (segundos ou data HTTP)
Como clientes devem usar: (1) verificar RateLimit-Remaining e desacelerar proativamente antes de chegar a 0 (evita o 429); (2) ao receber 429, aguardar exatamente Retry-After segundos com jitter (±10-20%) antes de tentar novamente — o jitter evita que múltiplos clientes tentem ao mesmo tempo; (3) implementar backoff exponencial em 429 consecutivos — se o retry após o Retry-After também retorna 429, o sistema ainda está sobrecarregado.
Erro comum: clientes que ignoram Retry-After e simplesmente retentam em loop imediatamente após 429 — isso aumenta a carga no servidor e prolonga o período de throttling. Outro erro: servidores que não incluem Retry-After em 429, deixando clientes sem saber quando tentar novamente.
Como você projetaria rate limiting para uma API que precisa de limites diferentes por tier de cliente (free/pro/enterprise)?
Estrutura de chaves Redis: rl:{api_key}:{window} — o contador por API key. Ao autenticar a API key (geralmente no middleware de auth), o sistema carrega os limites do tier associado: {"limit": 1000, "window": 3600, "burst": 50}. O limit é lido da configuração ou banco, não hardcoded no rate limiter.
Multi-dimensional: tipicamente os tiers diferem em múltiplas dimensões: (1) limite por hora/dia (quota); (2) limite por minuto (burst rate); (3) limites por endpoint específico; (4) limites por feature (ex: endpoints premium só para enterprise). Cada dimensão é um contador Redis separado com lógica independente.
Upgrade em tempo real: quando um cliente faz upgrade, o novo limite deve valer imediatamente (não esperar a janela resetar). Implemente isso armazenando o tier junto com o counter ou revalidando o tier em cada requisição (com cache de 1 minuto do tier na memória).
Headers de comunicação: retorne o limite do tier atual, não o limite global. O cliente free deve ver RateLimit-Limit: 1000 (seu limite), não o limite do tier enterprise. Inclua um campo de documentação indicando o endpoint de upgrade quando o cliente atinge o limite consistentemente.
Monitoramento: rastreie por API key qual percentual do limite está sendo utilizado. Clientes consistentemente no 90% do limite são candidatos a upgrade proativo — transforme rate limiting em input de product growth.
Referências
- artigo An alternative approach to rate limiting — Figma Engineering Blog (2017).
- artigo How Cloudflare uses rate limiting to protect services — Cloudflare Blog.
- docs Rate Limit Headers — IETF Draft.
- docs ASP.NET Core Rate Limiting middleware — Microsoft Learn.
- artigo Rate Limiting, Cells, and GCRA — Brandur Leach.
- padrão HTTP 429 Too Many Requests — RFC 6585 — IETF.
- blog Scaling Stripe's rate limiting infrastructure — Stripe Engineering.
- docs Kong Rate Limiting Plugin — Kong Gateway.
- docs Envoy Rate Limiting — Global Rate Limiting Service — Envoy Proxy.
- blog Handling overload — distributed rate limiting patterns — AWS Architecture Blog.
- artigo Circuit Breaker Pattern — Martin Fowler.
- livro Release It! — Design and Deploy Production-Ready Software — Michael T. Nygard (Pragmatic Bookshelf, 2ª ed. 2018).