Menu fechado

Implemente um Token Bucket Eficiente em FastAPI

Rate Limiter

🛡️ Protegendo APIs com Rate Limiting: O Conceito

A Necessidade de Resiliência em Ecossistemas FastAPI

No desenvolvimento de APIs modernas com FastAPI, a alta performance e a concorrência assíncrona baseada no Event Loop do Python são pilares fundamentais para a escalabilidade.
Entretanto, essa mesma eficiência pode se tornar um vetor de vulnerabilidade crítica se não houver mecanismos de controle de vazão (throttling).

O Rate Limiting não atua apenas como uma barreira de segurança contra ataques de negação de serviço (DDoS) ou força bruta; ele é, essencialmente, uma estratégia de governança de recursos e contenção de falhas.
Sem salvaguardas, um único cliente mal configurado ou um surto de tráfego automatizado por bots pode exaurir o pool de conexões do servidor ASGI e saturar a CPU.

Isso resulta no fenômeno de “Cascading Failures”, onde a degradação da latência em um endpoint afeta toda a malha de microserviços.
Implementar uma camada de controle de taxa garante que a infraestrutura permaneça previsível, protegendo o ciclo de vida da requisição desde o middleware até as camadas mais profundas de persistência de dados.

A Versatilidade do Algoritmo Token Bucket

A escolha do algoritmo Token Bucket para este guia baseia-se em seu equilíbrio excepcional entre simplicidade algorítmica e flexibilidade operacional.
O conceito baseia-se em um reservatório (bucket) que armazena tokens, onde cada token representa a autorização legal para o processamento de uma unidade de requisição.

O balde inicia com sua capacidade total, e cada chamada interceptada consome um crédito.
Simultaneamente, novos tokens são regenerados a uma taxa constante (refill rate) até atingir o limite máximo definido pelo administrador do sistema.

A grande vantagem técnica dessa abordagem é o suporte nativo a “bursts” (rajadas de tráfego).
Isso permite que aplicações web modernas, que costumam disparar múltiplas requisições paralelas para carregar recursos de UI, operem sem fricção, desde que o volume médio respeite a sustentabilidade do sistema a longo prazo.

Vantagens Técnicas Sobre Contadores de Janela Fixa

Diferente dos algoritmos de Janela Fixa (Fixed Window), que reiniciam contadores em intervalos rígidos, o Token Bucket opera de forma fluida através do tempo contínuo.
Algoritmos de janela fixa sofrem com o problema das “Bordas de Janela”, onde um atacante pode concentrar o dobro do tráfego permitido no exato momento da virada do cronômetro.

O Token Bucket mitiga esse risco ao utilizar o delta de tempo decorrido para calcular o refil de forma proporcional.
Para o arquiteto de software, isso oferece um controle granular sobre dois parâmetros críticos: a Burst Capacity (capacidade de absorção de picos) e o Sustained Throughput (vazão contínua).

Essa dualidade torna o Token Bucket a escolha padrão em infraestruturas de missão crítica, sendo a base de sistemas de alta escala como NGINX, Stripe e os Gateways de API da AWS.
Ao adotar este modelo, você garante que a API não apenas sobreviva a picos de tráfego, mas que o faça mantendo a justiça (fairness) entre os diversos consumidores do ecossistema.

🪣 Anatomia do Algoritmo Token Bucket

Mecânica de Funcionamento e Parâmetros Críticos

O algoritmo Token Bucket funciona como um modelo de controle de fluxo de estado variável, equilibrando a rigidez do limite com a dinâmica do tráfego real.
Para uma implementação robusta, é necessário parametrizar o sistema considerando a capacidade de processamento da infraestrutura subjacente.

  • Capacidade (Burst Capacity): Define o teto máximo de tokens acumulados. Representa a tolerância a picos instantâneos sem que o erro HTTP 429 seja disparado.
  • Taxa de Recarga (Sustained Throughput): Determina a velocidade de recuperação do balde. É o parâmetro que define o limite real de requisições por segundo (RPS) que o sistema suporta permanentemente.
  • Intervalo de Recarga (Temporal Resolution): Define a frequência de atualização do saldo. Intervalos menores (ex: milissegundos) oferecem uma experiência mais suave ao usuário final.

Considere uma API de pagamentos: se configurarmos um balde com capacidade de 10 tokens e recarga de 2 tokens/segundo, o cliente pode realizar 10 transações em milissegundos.
Após esse burst, ele fica restrito ao ritmo de processamento do servidor (2 por segundo), forçando o cliente a implementar estratégias de Backoff no lado da aplicação.

Parâmetro Função Técnica Impacto no Sistema
Capacity Buffer de Memória Absorve variações bruscas de latência do cliente.
Refill Rate Vazão Nominal Protege o banco de dados contra saturação de conexões.
Refill Interval Granularidade Evita o comportamento de “escada” na liberação de tráfego.

Comparativo de Estratégias de Throttling

Abaixo, comparamos as abordagens mais comuns no mercado para justificar a implementação do Token Bucket em ambientes FastAPI.

Algoritmo Mecânica de Bloqueio Custo de Infraestrutura
Token Bucket Consumo de créditos em balde com recarga passiva. Baixo: Exige apenas armazenamento de saldo e timestamp.
Fixed Window Contador resetado em intervalos discretos (ex: 1 min). Mínimo: Porém vulnerável a picos nas bordas da janela.
Sliding Window Cálculo baseado em lista de timestamps de cada req. Alto: Consumo de memória cresce linearmente com o tráfego.
Leaky Bucket Fila de processamento com saída em taxa constante. Médio: Adiciona latência artificial às requisições (Delay).

O diferencial do Token Bucket é a sua capacidade de não punir o tráfego legítimo que ocorre em rajadas, o que é o padrão em navegação mobile (onde o app dispara auth, profile e notifications simultaneamente).
Enquanto o Leaky Bucket forçaria um enfileiramento (que aumenta o tempo de resposta percebido), o Token Bucket permite o processamento imediato se houver crédito acumulado.

⚙️ Setup do Ambiente e Estrutura FastAPI

A implementação de um rate limiter robusto exige um ambiente de execução isolado para evitar poluição de dependências globais.
Utilizaremos o Python 3.9 ou superior para tirar proveito de melhorias em tipagem estática e performance do gerenciamento de memória.

Configuração do Workspace e Dependências

O isolamento térmico das dependências é garantido pelo uso de ambientes virtuais (VENV).
Este passo é crucial para simular um ambiente de produção onde a reprodutibilidade do build é obrigatória.

mkdir fastapi-ratelimit && cd fastapi-ratelimit
python -m venv venv

# No Linux/macOS:
source venv/bin/activate
# No Windows:
venv\Scripts\activate

Instalamos o FastAPI como framework core e o Uvicorn como servidor ASGI (Asynchronous Server Gateway Interface), que gerenciará o loop de eventos para as conexões não-bloqueantes.

pip install fastapi uvicorn

Arquitetura de Arquivos e Design de Software

Seguindo o princípio da Responsabilidade Única (SOLID), desacoplaremos a lógica algorítmica da infraestrutura de rede.
Isso permite que o motor de rate limiting seja testado unitariamente sem a necessidade de simular requisições HTTP complexas.

  • ratelimiter.py: Encapsula a inteligência do Token Bucket, cálculos de tempo e controle de concorrência.
  • main.py: Atua como o orquestrador, definindo rotas, middlewares e a injeção de dependência do limiter.

Implementação do Boilerplate FastAPI

O esqueleto inicial da aplicação estabelece o baseline de performance.
Utilizamos rotas assíncronas (async def) para garantir que o framework não bloqueie a thread principal durante a espera de I/O.

<!-- Não é código HTML, é Python comFastAPI -->
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello, world!"}

Não há erros de sintaxe internos no código fornecido. No entanto, ele não está escrito em HTML, mas sim em Python, utilizando a biblioteca FastAPI para criar uma aplicação web. O código parece estar correto e pronto para uso em um projeto FastAPI.

Se você quiser um exemplo de código HTML, aqui está um exemplo simples:

<html>
  <head>
    <title>Título</title>
  </head>
  <body>
    <h1>Hello, World!</h1>
  </body>
</html>

Para o desenvolvimento produtivo, iniciamos o Uvicorn com o Hot Reloading habilitado.
Isso permite que cada alteração no algoritmo de recarga de tokens seja refletida instantaneamente sem reiniciar o processo do servidor.

uvicorn main:app --reload

Com o status 200 confirmado, temos a fundação necessária para injetar o controle de fluxo.
O próximo passo envolverá a manipulação de primitivas de sincronização para garantir que o rate limit seja thread-safe.

💻 Implementando a Lógica Core: A Classe TokenBucket

A precisão matemática e a segurança em ambientes concorrentes são as maiores dificuldades ao implementar um rate limiter manual.
Em aplicações FastAPI, múltiplas requisições assíncronas podem acessar o mesmo objeto de estado, criando o risco de condições de corrida (Race Conditions).

import time
import threading

class TokenBucket:
    """
    Token Bucket rate limiter.
    Each bucket starts full at `max_tokens` and refills `refill_rate` tokens every `interval` seconds, up to the maximum capacity.
    """
    def __init__(self, max_tokens: int, refill_rate: int, interval: float):
        assert max_tokens > 0, "max_tokens must be positive"
        assert refill_rate > 0, "refill_rate must be positive"
        assert interval > 0, "interval must be positive"
        
        self.max_tokens = max_tokens
        self.refill_rate = refill_rate
        self.interval = interval
        self.tokens = max_tokens
        self.refilled_at = time.time()
        self.lock = threading.Lock()

    def _refill(self):
        """Add tokens based on elapsed time since the last refill."""
        now = time.time()
        elapsed = now - self.refilled_at
        if elapsed >= self.interval:
            num_refills = int(elapsed // self.interval)
            self.tokens = min(
                self.max_tokens, self.tokens + num_refills * self.refill_rate
            )
            self.refilled_at += num_refills * self.interval

    def allow_request(self, tokens: int = 1) -> bool:
        """
        Attempt to consume `tokens` from the bucket.
        Returns True if the request is allowed, False if the bucket does not have enough tokens.
        """
        with self.lock:
            self._refill()
            if self.tokens >= tokens:
                self.tokens -= tokens
                return True
            return False

    def get_remaining(self) -> int:
        """Return the current number of available tokens."""
        with self.lock:
            self._refill()
            return self.tokens

    def get_reset_time(self) -> float:
        """Return the Unix timestamp when the next refill occurs."""
        with self.lock:
            return self.refilled_at + self.interval

Gerenciamento de Concorrência com threading.Lock

Embora o FastAPI seja assíncrono, a mutação de estado compartilhado (como o saldo de tokens) exige proteção atômica.
O uso do threading.Lock() assegura que a sequência de leitura, cálculo de refil e decremento de tokens ocorra sem interrupções de outras threads.

Sem esse bloqueio, dois processos simultâneos poderiam ler o mesmo saldo de 1 token, permitir ambas as requisições, e deixar o balde com um saldo inconsistente de -1.
Isso causaria um bypass do limite, permitindo que um atacante ultrapasse as quotas estabelecidas através de paralelismo massivo.

O Mecanismo de Refill Passivo (Lazy Evaluation)

Em vez de criar processos em background (Background Tasks) que consomem ciclos de CPU constantes para atualizar os baldes, utilizamos a avaliação preguiçosa.
O balde só calcula quanto tempo se passou desde a última requisição no exato momento em que uma nova chamada chega.

A expressão self.refilled_at += num_refills * self.interval é um detalhe de engenharia sutil, mas vital.
Ela preserva o resto da divisão do tempo decorrido, evitando o “Drift Temporal” que ocorreria se simplesmente resetássemos o cronômetro para o tempo atual (time.time()).

Exposição de Metadados para Governança

Uma API profissional deve ser transparente sobre suas limitações.
Os métodos get_remaining e get_reset_time permitem que o middleware informe ao cliente exatamente quanto de sua quota resta e quando ela será renovada.

Essas informações são fundamentais para que integradores implementem mecanismos de retentativa inteligentes.
Ao expor esses dados, reduzimos o número de requisições inúteis que atingiriam o servidor apenas para receber um erro, economizando largura de banda e processamento.

👥 Gerenciamento de Estado Per-User com RateLimiterStore

Limites globais de API são ineficientes em cenários de múltiplos clientes.
Um rate limiter eficaz deve isolar a atividade de cada ator, garantindo que o comportamento abusivo de um usuário não cause negação de serviço para outros consumidores legítimos.

Isolamento de Quotas e Identificação de Identidade

A classe RateLimiterStore funciona como um Registry centralizado.
Ela gerencia um dicionário de instâncias de TokenBucket, onde a chave de busca é tipicamente o endereço IP ou um identificador de conta (API Key).




from collections import defaultdict
import threading

class TokenBucket:
    """
    Implementação de um bucket de tokens.
    """
    def __init__(self, max_tokens: int, refill_rate: int, interval: float):
        self.max_tokens = max_tokens
        self.refill_rate = refill_rate
        self.interval = interval
        self._tokens = max_tokens
        self._last_refill = 0
        self._lock = threading.Lock()

    def consume(self, amount: int) -> bool:
        """
        Consume tokens do bucket.
        """
        with self._lock:
            if self._tokens >= amount:
                self._tokens -= amount
                return True
            else:
                return False

    def refill(self) -> None:
        """
        Refaz o bucket de tokens.
        """
        with self._lock:
            current_time = time.time()
            elapsed_time = current_time - self._last_refill
            if elapsed_time > self.interval:
                self._tokens = min(self.max_tokens, self._tokens + elapsed_time * self.refill_rate / self.interval)
                self._last_refill = current_time

class RateLimiterStore:
    """
    Gerencia Buckets de Token por usuário.
    Cada chave única de cliente (ex: endereço IP) recebe seu próprio bucket
    com parâmetros idênticos.
    """
    def __init__(self, max_tokens: int, refill_rate: int, interval: float):
        self.max_tokens = max_tokens
        self.refill_rate = refill_rate
        self.interval = interval
        self._buckets: dict[str, TokenBucket] = {}
        self._lock = threading.Lock()

    def get_bucket(self, key: str) -> TokenBucket:
        """
        Retorna o TokenBucket para uma determinada chave de cliente.
        Cria um novo bucket se ele ainda não existir.
        """
        with self._lock:
            if key not in self._buckets:
                self._buckets[key] = TokenBucket(
                    max_tokens=self.max_tokens,
                    refill_rate=self.refill_rate,
                    interval=self.interval,
                )
            return self._buckets[key]

A criação dinâmica de buckets deve ser protegida por locks.
Isso evita a “Instanciação Duplicada”, onde o sistema poderia criar dois baldes independentes para o mesmo IP se as requisições chegassem em um intervalo de microssegundos.

Desafios de Persistência em Memória

Nesta implementação, o estado reside na memória RAM da aplicação.
Isso oferece a menor latência possível (sub-milissegundo), o que é ideal para middlewares que não podem adicionar overhead à requisição.

Contudo, arquitetos devem estar cientes de que, em ambientes de deploy com múltiplos workers (Gunicorn/Uvicorn), cada processo terá seu próprio RateLimiterStore.
Para uma sincronização perfeita entre réplicas em clusters Kubernetes, o armazenamento evoluiria para um backend distribuído como Redis, mantendo a mesma interface lógica aqui definida.

🔌 Integração via Middleware e Headers HTTP

O Papel do Middleware como Gatekeeper

O middleware é a camada de interceptação que executa a lógica de controle antes que a requisição atinja a lógica de negócio (Path Operations).
No FastAPI, utilizamos o decorador @app.middleware("http") para envolver todo o ciclo de vida da chamada HTTP.

import time
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from ratelimiter import RateLimiterStore

app = FastAPI()

# Configuração: burst de 10 requisições, recarga de 2 tokens a cada 1 segundo.
limiter = RateLimiterStore(max_tokens=10, refill_rate=2, interval=1.0)

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    """
    Middleware para aplicação de rate limiting por IP.
    Garante a conformidade com headers X-RateLimit.
    """
    client_ip = request.client.host
    bucket = limiter.get_bucket(client_ip)

    # Verifica disponibilidade de tokens
    if not bucket.allow_request():
        retry_after = bucket.get_reset_time() - time.time()
        return JSONResponse(
            status_code=429,
            content={"detail": "Too many requests. Try again later."},
            headers={
                "Retry-After": str(max(1, int(retry_after))),
                "X-RateLimit-Limit": str(bucket.max_tokens),
                "X-RateLimit-Remaining": str(bucket.get_remaining()),
                "X-RateLimit-Reset": str(int(bucket.get_reset_time())),
            },
        )

    # Processamento da requisição em caso de sucesso
    response = await call_next(request)
    
    # Injeção de headers informativos na resposta positiva
    response.headers["X-RateLimit-Limit"] = str(bucket.max_tokens)
    response.headers["X-RateLimit-Remaining"] = str(bucket.get_remaining())
    response.headers["X-RateLimit-Reset"] = str(int(bucket.get_reset_time()))
    
    return response

@app.get("/")
async def root():
    return {"message": "Hello, world!"}

O middleware recupera o host do cliente através do objeto request.client.host.
Esta é a primeira linha de defesa; se o bucket associado a esse host não possuir tokens, a execução é abortada imediatamente com um HTTP 429 Too Many Requests.

Semântica de Resposta e Padronização IETF

A conformidade com padrões de mercado aumenta a interoperabilidade da sua API.
Injetamos headers que permitem ao cliente entender sua situação de consumo sem precisar adivinhar as regras de negócio do servidor:

  • Retry-After: Tempo em segundos até que ao menos um token seja reposto no balde.
  • X-RateLimit-Limit: A Burst Capacity total definida no setup do ambiente.
  • X-RateLimit-Remaining: A quantidade de tokens disponíveis após o processamento da chamada atual.
  • X-RateLimit-Reset: Timestamp Unix indicando a renovação completa do balde.

Essa abordagem transforma o rate limiting de uma punição em uma ferramenta de comunicação entre sistemas, reduzindo drasticamente o tráfego de erro em produção.

🧪 Estratégias de Teste: Burst e Refill

Testar rate limiters exige simular cenários de alta concorrência que uma simples atualização de navegador não consegue reproduzir.
Precisamos validar se o balde esgota exatamente no limite e se a taxa de recarga respeita o intervalo configurado.

Auditoria de Headers com cURL

O cURL é a ferramenta de auditoria técnica por excelência.
Utilize a flag -I para inspecionar apenas os cabeçalhos de resposta e validar a injeção dos metadados de limite.

curl -i http://127.0.0.1:8000/data

A saída deve revelar o decréscimo progressivo do header x-ratelimit-remaining.
Qualquer anomalia aqui indica uma falha na lógica de persistência do estado no RateLimiterStore.

HTTP/1.1 200 OK
x-ratelimit-limit: 10
x-ratelimit-remaining: 9
x-ratelimit-reset: 1739836801
{"data": "Some important information"}

Automatizando o Estresse com Python Requests

Para validar o comportamento de burst, utilizamos um script que dispara requisições em loop.
Isso testa a robustez do middleware sob carga constante.

“`python
import requests
import time

def test_burst():
“””Simula um burst de 15 requisições rápidas para forçar o HTTP 429.”””
url = “http://127.0.0.1:8000/data”
results = []

print(“=== Iniciando Teste de Burst ===”)
for i in range(15):
response = requests.get(url)
remaining = response.headers.get(“X-RateLimit-Remaining”, “N/A”)
results.append((i + 1, response.status_code, remaining))
print(f”Request {i+1:2d} | Status: {response.status_code} | Restante: {remaining}”)

allowed = sum(1 for _, status, _ in results if status == 200)
blocked = sum(1 for _, status, _ in results if status == 429)
print(f”\nResumo: Permitidas: {allowed}, Bloqueadas: {blocked}\n”)

def test_refill():
“””Esgota o bucket e valida a recuperação após o intervalo de recarga.”””
url = “http://127.0.0.1:8000/data”

print(“=== Iniciando Teste de Refill ===”)
print(“Esgotando tokens…”)
for _ in range(12):
requests.get(url)

print(“Aguardando 3 segundos para recarga (intervalo de refill)…”)
time.sleep(3)

print(“Enviando novas requisições após o refill:”)
for i in range(3):
response = requests.get(url)
remaining = response.headers.get(“X-RateLimit-Remaining”, “N/A”)
print(f”Request {i+1:2d} | Status: {response.status_code} | Restante: {remaining}”)

if __name__ == “__main__”:
# Certifique-se de que o Uvicorn está rodando antes de executar
test_burst()
time.sleep(5) # Delay entre testes para reset do bucket
test_refill()
“`

O sucesso do teste é definido por uma transição limpa entre o código 200 e o 429.
Se o sistema permitir a 11ª requisição em um balde de capacidade 10, existe um erro de “Off-by-one” na lógica de comparação do algoritmo.

🏗️ Considerações de Arquitetura e Próximos Passos

Hierarquia de Defesa: Gateway vs. Aplicação

Uma arquitetura de resiliência moderna utiliza defesa em profundidade.
Enquanto o limite no nível da aplicação (este tutorial) resolve regras de negócio complexas, o API Gateway (como Kong ou NGINX) deve filtrar ataques volumétricos de infraestrutura.

A lógica na camada de aplicação é ideal para limites baseados em planos de assinatura (Tiered Limiting), onde usuários “Premium” possuem buckets maiores do que usuários “Free”.
O Gateway, por outro lado, protege o sistema contra IPs maliciosos conhecidos antes mesmo de a requisição tocar o código Python.

Escalabilidade Distribuída com Redis

Para sistemas que rodam em múltiplas instâncias, o dicionário local torna-se um gargalo de consistência.
A evolução natural é substituir o RateLimiterStore por um cliente Redis.

Ao migrar para o Redis, utilize Scripts Lua (EVAL) para garantir a atomicidade das operações.
Isso evita que o delay de rede entre sua aplicação e o banco de dados crie novas janelas para race conditions, mantendo a integridade do rate limit mesmo em escalas globais.

Identificação Avançada de Clientes

Identificar usuários por IP (request.client.host) é arriscado devido ao uso de NATs corporativos e proxies.
Em ambientes autenticados, extraia o identificador do cabeçalho Authorization (JWT) ou de uma API-Key customizada.

# Exemplo de adaptação para identificação via API Key
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    # Tenta obter a API Key do header, fallback para IP se ausente
    client_key = request.headers.get("X-API-Key") or request.client.host
    
    bucket = limiter.get_bucket(client_key)
    if not bucket.allow_request():
        # ... lógica de retorno 429 ...
        pass
    
    return await call_next(request)

Ao evoluir para identificação por ID de usuário, o rate limiting torna-se uma ferramenta de monetização e controle de custos de infraestrutura, permitindo uma gestão financeira mais precisa sobre o consumo de recursos da nuvem.


Fonte: freecodecamp.org.
Curadoria e Insights: Redação YTI&W (Developers).



Redação YTI&W-News

Redação Developers | Yassutaro TI & Web

Notícias do universo do Desenvolvimento Web, dicas e tutoriais para Webmasters.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Publicado em:APIs e Integrações,Desenvolvimento de Software,Segurança,Tutoriais Práticos
Fale Conosco
×

Inscreva-se em nossa Newsletter!


Receba nossos lançamentos e artigos em primera mão!