MÓDULO 09 · CONCEITO 13 DE 14

Rate Limiting

Algoritmos de controle de tráfego, headers padrão, throttling distribuído e proteção contra abuso em APIs

Tempo de leitura ~22 min Pré-requisito 06 · API Gateway · 08 · Disponibilidade & Resiliência Próximo 14 · Observabilidade em Comunicação Distribuída

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
nota Na prática de APIs web, token bucket é o algoritmo mais flexível e mais implementado — permite bursts curtos sem penalizar clientes legítimos, enquanto protege o sistema downstream. Sliding window counter é preferido quando a precisão é crítica e os limites não permitem nenhuma tolerância a burst.

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.

atenção Se o Redis estiver indisponível, você tem duas opções: fail-open (permitir todas as requisições — sistema sem proteção) ou fail-closed (bloquear todas as requisições — serviço offline para todos). Fail-open é preferível para APIs de produção — a perda temporária de rate limiting é menos grave que a indisponibilidade total. Implemente um circuit breaker para o cliente Redis com fallback fail-open e alerta de monitoramento.

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# — ASP.NET Core RateLimiter
// 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 — FastAPI + 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 — golang.org/x/time/rate
// 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

Algoritmo: Fixed Window vs Sliding Window vs Token Bucket

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.

Rate limiting por IP vs por API key vs por usuário

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.

Rate limiter in-process vs distribuído (Redis)

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 vs Circuit Breaker vs Throttling

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.

  1. 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.
  2. 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.
  3. 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 /orders especificamente. Implemente os headers RateLimit-Limit, RateLimit-Remaining e Retry-After em 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.
  4. Implemente o retry client correto: um cliente que lê o header Retry-After de 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.
  5. Meça o overhead de latência do rate limiting distribuído vs in-process: com um benchmark de 10.000 req/s usando k6 ou wrk, 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

  1. artigo An alternative approach to rate limiting — Figma Engineering Blog (2017). figma.com/blog/an-alternative-approach-to-rate-limiting — Apresenta o sliding window counter com interpolação linear como alternativa ao fixed window, com análise do trade-off de precisão vs custo. A implementação Lua é diretamente aplicável.
  2. artigo How Cloudflare uses rate limiting to protect services — Cloudflare Blog. blog.cloudflare.com/counting-things-a-lot-of-different-things — Detalha o algoritmo de sliding window counter usado pelo Cloudflare em escala global, com análise de precisão e comparação com token bucket.
  3. docs Rate Limit Headers — IETF Draft. ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-07.txt — Especificação IETF para headers RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset. Define semântica precisa para cada header e comportamento esperado dos clientes.
  4. docs ASP.NET Core Rate Limiting middleware — Microsoft Learn. learn.microsoft.com/en-us/aspnet/core/performance/rate-limit — Documentação completa do RateLimiter built-in do .NET 7+, incluindo todos os algoritmos disponíveis, configuração por endpoint e políticas de rejeição customizadas.
  5. artigo Rate Limiting, Cells, and GCRA — Brandur Leach. brandur.org/rate-limiting — Análise do Generic Cell Rate Algorithm (GCRA), equivalente ao leaky bucket, com implementação em Go e comparação com token bucket. Inclui discussão sobre rate limiting em APIs de pagamento.
  6. padrão HTTP 429 Too Many Requests — RFC 6585 — IETF. datatracker.ietf.org/doc/html/rfc6585#section-4 — Definição formal do status code 429, o header Retry-After obrigatório, e a semântica de "rate limit" no protocolo HTTP. Base normativa para qualquer implementação de rate limiting em APIs.
  7. blog Scaling Stripe's rate limiting infrastructure — Stripe Engineering. stripe.com/blog/rate-limiters — Descreve a evolução do rate limiting na Stripe: de in-process para Redis distribuído, algoritmos usados (token bucket), e como o sistema de rate limiting em si foi tornado resiliente a falhas do Redis (fail-open).
  8. docs Kong Rate Limiting Plugin — Kong Gateway. docs.konghq.com/hub/kong-inc/rate-limiting — Documentação de um rate limiter de gateway em produção: configuração por consumer/route/service, algoritmos suportados, múltiplos backends de storage (Redis, local, cluster), e headers de resposta incluídos. Boa referência de interface para implementar o seu.
  9. docs Envoy Rate Limiting — Global Rate Limiting Service — Envoy Proxy. www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting — Como o Envoy delega rate limiting para um serviço externo via gRPC (ratelimit service). Permite rate limiting centralizado e distribuído para toda a malha de serviços, com políticas definidas em YAML.
  10. blog Handling overload — distributed rate limiting patterns — AWS Architecture Blog. Análise das estratégias de proteção contra overload em sistemas distribuídos: client-side throttling (o cliente detecta rejeições e reduz a taxa), adaptive concurrency limits (Hystrix/Concurrency Limiter), e como combinar rate limiting com backpressure para evitar cascatas.
  11. artigo Circuit Breaker Pattern — Martin Fowler. martinfowler.com/bliki/CircuitBreaker.html — O artigo de referência de Martin Fowler que popularizou o Circuit Breaker pattern. Explica os três estados (closed, open, half-open), threshold de abertura, e por que o circuit breaker complementa (não substitui) o rate limiting.
  12. livro Release It! — Design and Deploy Production-Ready Software — Michael T. Nygard (Pragmatic Bookshelf, 2ª ed. 2018). Capítulos sobre Stability Patterns: Circuit Breaker, Bulkhead, Timeouts, e por que sistemas falham em cascata. O capítulo de Throttling e Backpressure é diretamente aplicável ao design de rate limiters que protegem a estabilidade do sistema, não apenas limitam o cliente.