Rate Limiter Middleware em Rust

Construa um rate limiter com algoritmo token bucket como middleware Axum em Rust, com limite por IP, headers HTTP e resposta 429.

Neste projeto vamos construir um rate limiter (limitador de taxa) como middleware para Axum usando o algoritmo token bucket. O middleware vai limitar requisicoes por endereco IP, retornar headers HTTP padrao indicando limites e requisicoes restantes, e responder com codigo 429 (Too Many Requests) quando o limite for excedido. Rate limiting e essencial para proteger APIs contra abuso, ataques DDoS e garantir distribuicao justa de recursos.

O algoritmo token bucket e simples e eficiente: cada cliente possui um “balde” com uma quantidade maxima de tokens. Cada requisicao consome um token. Os tokens sao reabastecidos ao longo do tempo a uma taxa configuravel. Se o balde estiver vazio, a requisicao e rejeitada.

O Que Vamos Construir

Um middleware de rate limiting com as seguintes funcionalidades:

  • Algoritmo token bucket com taxa e capacidade configuraveis
  • Limite por endereco IP do cliente
  • Headers HTTP padrao: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
  • Resposta 429 Too Many Requests com header Retry-After
  • Limpeza automatica de buckets inativos
  • Rotas de exemplo para demonstrar o comportamento

Estrutura do Projeto

rate-limiter/
├── Cargo.toml
└── src/
    └── main.rs

Configurando o Projeto

cargo new rate-limiter
cd rate-limiter

Configure o Cargo.toml:

[package]
name = "rate-limiter"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Este projeto usa apenas as dependencias fundamentais, pois o rate limiter e implementado do zero.

Passo 1: Implementando o Token Bucket

O nucleo do rate limiter e a estrutura TokenBucket:

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Instant;

// Configuracao do rate limiter
#[derive(Clone)]
pub struct ConfigRateLimiter {
    pub capacidade_maxima: u32,      // Maximo de tokens no balde
    pub tokens_por_segundo: f64,     // Taxa de reabastecimento
    pub janela_segundos: u64,        // Janela para o header Reset
}

impl Default for ConfigRateLimiter {
    fn default() -> Self {
        ConfigRateLimiter {
            capacidade_maxima: 10,
            tokens_por_segundo: 1.0,
            janela_segundos: 60,
        }
    }
}

// Balde de tokens para um cliente
struct TokenBucket {
    tokens: f64,
    ultimo_acesso: Instant,
    capacidade_maxima: u32,
    tokens_por_segundo: f64,
}

impl TokenBucket {
    fn novo(config: &ConfigRateLimiter) -> Self {
        TokenBucket {
            tokens: config.capacidade_maxima as f64,
            ultimo_acesso: Instant::now(),
            capacidade_maxima: config.capacidade_maxima,
            tokens_por_segundo: config.tokens_por_segundo,
        }
    }

    // Reabastecer tokens com base no tempo decorrido
    fn reabastecer(&mut self) {
        let agora = Instant::now();
        let tempo_decorrido = agora.duration_since(self.ultimo_acesso).as_secs_f64();
        self.tokens = (self.tokens + tempo_decorrido * self.tokens_por_segundo)
            .min(self.capacidade_maxima as f64);
        self.ultimo_acesso = agora;
    }

    // Tenta consumir um token. Retorna true se conseguiu.
    fn consumir(&mut self) -> bool {
        self.reabastecer();
        if self.tokens >= 1.0 {
            self.tokens -= 1.0;
            true
        } else {
            false
        }
    }

    // Retorna quantos tokens restam (arredondado para baixo)
    fn restantes(&self) -> u32 {
        self.tokens as u32
    }
}

// Estado compartilhado do rate limiter
#[derive(Clone)]
pub struct RateLimiter {
    buckets: Arc<Mutex<HashMap<String, TokenBucket>>>,
    config: ConfigRateLimiter,
}

impl RateLimiter {
    pub fn novo(config: ConfigRateLimiter) -> Self {
        RateLimiter {
            buckets: Arc::new(Mutex::new(HashMap::new())),
            config,
        }
    }

    // Verifica e consome um token para o IP dado
    // Retorna (permitido, tokens_restantes)
    pub fn verificar(&self, ip: &str) -> (bool, u32) {
        let mut buckets = self.buckets.lock().unwrap();
        let bucket = buckets
            .entry(ip.to_string())
            .or_insert_with(|| TokenBucket::novo(&self.config));

        let permitido = bucket.consumir();
        let restantes = bucket.restantes();
        (permitido, restantes)
    }

    // Remove buckets inativos ha mais de 5 minutos
    pub fn limpar_inativos(&self) {
        let mut buckets = self.buckets.lock().unwrap();
        let limite = Instant::now() - std::time::Duration::from_secs(300);
        buckets.retain(|_, bucket| bucket.ultimo_acesso > limite);
    }
}

A funcao reabastecer calcula quantos tokens adicionar com base no tempo desde o ultimo acesso. Isso garante que tokens sejam adicionados continuamente sem precisar de um timer separado.

Passo 2: Criando o Middleware Axum

Agora transformamos o rate limiter em um middleware Axum:

use axum::{
    extract::Request,
    http::{HeaderMap, HeaderValue, StatusCode},
    middleware::Next,
    response::{IntoResponse, Response},
    Json,
};

// Extrai o IP do cliente da requisicao
fn extrair_ip(req: &Request) -> String {
    // Tenta o header X-Forwarded-For primeiro (para proxies)
    if let Some(forwarded) = req.headers().get("x-forwarded-for") {
        if let Ok(valor) = forwarded.to_str() {
            if let Some(primeiro_ip) = valor.split(',').next() {
                return primeiro_ip.trim().to_string();
            }
        }
    }

    // Tenta o header X-Real-IP
    if let Some(real_ip) = req.headers().get("x-real-ip") {
        if let Ok(valor) = real_ip.to_str() {
            return valor.to_string();
        }
    }

    // Fallback: usa um IP padrao
    "127.0.0.1".to_string()
}

// Middleware de rate limiting
async fn middleware_rate_limiter(
    axum::extract::State(limiter): axum::extract::State<RateLimiter>,
    req: Request,
    next: Next,
) -> Response {
    let ip = extrair_ip(&req);
    let (permitido, restantes) = limiter.verificar(&ip);

    let mut headers = HeaderMap::new();
    headers.insert(
        "X-RateLimit-Limit",
        HeaderValue::from_str(&limiter.config.capacidade_maxima.to_string()).unwrap(),
    );
    headers.insert(
        "X-RateLimit-Remaining",
        HeaderValue::from_str(&restantes.to_string()).unwrap(),
    );
    headers.insert(
        "X-RateLimit-Reset",
        HeaderValue::from_str(&limiter.config.janela_segundos.to_string()).unwrap(),
    );

    if !permitido {
        // Calcula tempo ate o proximo token
        let retry_after = (1.0 / limiter.config.tokens_por_segundo).ceil() as u64;
        headers.insert(
            "Retry-After",
            HeaderValue::from_str(&retry_after.to_string()).unwrap(),
        );

        let corpo = Json(serde_json::json!({
            "erro": "Muitas requisicoes. Tente novamente mais tarde.",
            "retry_after_segundos": retry_after
        }));

        let mut resposta = (StatusCode::TOO_MANY_REQUESTS, corpo).into_response();
        resposta.headers_mut().extend(headers);
        return resposta;
    }

    let mut resposta = next.run(req).await;
    resposta.headers_mut().extend(headers);
    resposta
}

O middleware adiciona headers de rate limiting a todas as respostas, tanto as permitidas quanto as rejeitadas. O header Retry-After indica ao cliente quanto tempo esperar antes de tentar novamente.

Passo 3: Rotas de Exemplo e Limpeza Automatica

Vamos criar rotas de exemplo e um task de limpeza:

use axum::routing::get;
use axum::Router;

// Endpoint de exemplo: recurso publico
async fn recurso_publico() -> impl IntoResponse {
    Json(serde_json::json!({
        "mensagem": "Recurso publico acessado com sucesso!",
        "dados": [1, 2, 3, 4, 5]
    }))
}

// Endpoint de exemplo: recurso com dados maiores
async fn recurso_pesado() -> impl IntoResponse {
    Json(serde_json::json!({
        "mensagem": "Recurso pesado processado!",
        "resultados": (0..100).collect::<Vec<i32>>()
    }))
}

// Endpoint de status do rate limiter
async fn status_rate_limiter() -> impl IntoResponse {
    Json(serde_json::json!({
        "servico": "Rate Limiter",
        "status": "ativo",
        "algoritmo": "Token Bucket"
    }))
}

// Inicia task de limpeza periodica de buckets inativos
fn iniciar_limpeza(limiter: RateLimiter) {
    tokio::spawn(async move {
        loop {
            tokio::time::sleep(std::time::Duration::from_secs(60)).await;
            limiter.limpar_inativos();
            println!("Limpeza de buckets inativos realizada.");
        }
    });
}

A limpeza periodica evita que o HashMap cresca indefinidamente com IPs que nao fazem mais requisicoes.

Passo 4: Montando o main.rs Completo

Aqui esta o codigo completo do src/main.rs:

use axum::{
    extract::{Request, State},
    http::{HeaderMap, HeaderValue, StatusCode},
    middleware::{self, Next},
    response::{IntoResponse, Response},
    routing::get,
    Json, Router,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Instant;

// === Token Bucket ===

#[derive(Clone)]
pub struct ConfigRateLimiter {
    pub capacidade_maxima: u32,
    pub tokens_por_segundo: f64,
    pub janela_segundos: u64,
}

impl Default for ConfigRateLimiter {
    fn default() -> Self {
        ConfigRateLimiter {
            capacidade_maxima: 10,
            tokens_por_segundo: 1.0,
            janela_segundos: 60,
        }
    }
}

struct TokenBucket {
    tokens: f64,
    ultimo_acesso: Instant,
    capacidade_maxima: u32,
    tokens_por_segundo: f64,
}

impl TokenBucket {
    fn novo(config: &ConfigRateLimiter) -> Self {
        TokenBucket {
            tokens: config.capacidade_maxima as f64,
            ultimo_acesso: Instant::now(),
            capacidade_maxima: config.capacidade_maxima,
            tokens_por_segundo: config.tokens_por_segundo,
        }
    }

    fn reabastecer(&mut self) {
        let agora = Instant::now();
        let decorrido = agora.duration_since(self.ultimo_acesso).as_secs_f64();
        self.tokens = (self.tokens + decorrido * self.tokens_por_segundo)
            .min(self.capacidade_maxima as f64);
        self.ultimo_acesso = agora;
    }

    fn consumir(&mut self) -> bool {
        self.reabastecer();
        if self.tokens >= 1.0 {
            self.tokens -= 1.0;
            true
        } else {
            false
        }
    }

    fn restantes(&self) -> u32 {
        self.tokens as u32
    }
}

#[derive(Clone)]
pub struct RateLimiter {
    buckets: Arc<Mutex<HashMap<String, TokenBucket>>>,
    config: ConfigRateLimiter,
}

impl RateLimiter {
    pub fn novo(config: ConfigRateLimiter) -> Self {
        RateLimiter {
            buckets: Arc::new(Mutex::new(HashMap::new())),
            config,
        }
    }

    pub fn verificar(&self, ip: &str) -> (bool, u32) {
        let mut buckets = self.buckets.lock().unwrap();
        let bucket = buckets
            .entry(ip.to_string())
            .or_insert_with(|| TokenBucket::novo(&self.config));
        let permitido = bucket.consumir();
        let restantes = bucket.restantes();
        (permitido, restantes)
    }

    pub fn limpar_inativos(&self) {
        let mut buckets = self.buckets.lock().unwrap();
        let limite = Instant::now() - std::time::Duration::from_secs(300);
        buckets.retain(|_, b| b.ultimo_acesso > limite);
    }
}

// === Middleware ===

fn extrair_ip(req: &Request) -> String {
    if let Some(forwarded) = req.headers().get("x-forwarded-for") {
        if let Ok(valor) = forwarded.to_str() {
            if let Some(ip) = valor.split(',').next() {
                return ip.trim().to_string();
            }
        }
    }
    if let Some(real_ip) = req.headers().get("x-real-ip") {
        if let Ok(valor) = real_ip.to_str() {
            return valor.to_string();
        }
    }
    "127.0.0.1".to_string()
}

async fn middleware_rate_limiter(
    State(limiter): State<RateLimiter>,
    req: Request,
    next: Next,
) -> Response {
    let ip = extrair_ip(&req);
    let (permitido, restantes) = limiter.verificar(&ip);

    let mut headers = HeaderMap::new();
    headers.insert(
        "X-RateLimit-Limit",
        HeaderValue::from_str(&limiter.config.capacidade_maxima.to_string()).unwrap(),
    );
    headers.insert(
        "X-RateLimit-Remaining",
        HeaderValue::from_str(&restantes.to_string()).unwrap(),
    );
    headers.insert(
        "X-RateLimit-Reset",
        HeaderValue::from_str(&limiter.config.janela_segundos.to_string()).unwrap(),
    );

    if !permitido {
        let retry_after = (1.0 / limiter.config.tokens_por_segundo).ceil() as u64;
        headers.insert(
            "Retry-After",
            HeaderValue::from_str(&retry_after.to_string()).unwrap(),
        );

        let corpo = Json(serde_json::json!({
            "erro": "Muitas requisicoes. Tente novamente mais tarde.",
            "retry_after_segundos": retry_after
        }));

        let mut resposta = (StatusCode::TOO_MANY_REQUESTS, corpo).into_response();
        resposta.headers_mut().extend(headers);
        return resposta;
    }

    let mut resposta = next.run(req).await;
    resposta.headers_mut().extend(headers);
    resposta
}

// === Handlers ===

async fn recurso_publico() -> impl IntoResponse {
    Json(serde_json::json!({
        "mensagem": "Recurso publico acessado com sucesso!",
        "dados": [1, 2, 3, 4, 5]
    }))
}

async fn recurso_pesado() -> impl IntoResponse {
    Json(serde_json::json!({
        "mensagem": "Recurso pesado processado!",
        "itens": 100
    }))
}

async fn status() -> impl IntoResponse {
    Json(serde_json::json!({
        "servico": "Rate Limiter Demo",
        "status": "ativo",
        "algoritmo": "Token Bucket"
    }))
}

// === Main ===

#[tokio::main]
async fn main() {
    let config = ConfigRateLimiter {
        capacidade_maxima: 5,       // 5 requisicoes iniciais
        tokens_por_segundo: 0.5,    // 1 token a cada 2 segundos
        janela_segundos: 60,
    };

    let limiter = RateLimiter::novo(config);

    // Inicia limpeza periodica
    let limiter_limpeza = limiter.clone();
    tokio::spawn(async move {
        loop {
            tokio::time::sleep(std::time::Duration::from_secs(60)).await;
            limiter_limpeza.limpar_inativos();
        }
    });

    let app = Router::new()
        .route("/api/publico", get(recurso_publico))
        .route("/api/pesado", get(recurso_pesado))
        .route("/status", get(status))
        .layer(middleware::from_fn_with_state(
            limiter.clone(),
            middleware_rate_limiter,
        ));

    let endereco = "0.0.0.0:3000";
    println!("Rate Limiter rodando em http://{}", endereco);
    println!("Limite: 5 requisicoes, reabastecendo 1 a cada 2 segundos");

    let listener = tokio::net::TcpListener::bind(endereco).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Como Executar

Compile e inicie o servidor:

cargo run

Teste o rate limiting fazendo varias requisicoes rapidas:

# Primeiras 5 requisicoes passam normalmente
for i in $(seq 1 7); do
    echo "--- Requisicao $i ---"
    curl -s -D - http://localhost:3000/api/publico 2>&1 | head -10
    echo
done

# Observe os headers na resposta:
# X-RateLimit-Limit: 5
# X-RateLimit-Remaining: 4   (decrescendo a cada requisicao)
# X-RateLimit-Reset: 60

# Apos esgotar os tokens:
# HTTP/1.1 429 Too Many Requests
# Retry-After: 2
# {"erro":"Muitas requisicoes. Tente novamente mais tarde.","retry_after_segundos":2}

# Espere 2 segundos e tente novamente - voltara a funcionar
sleep 2 && curl http://localhost:3000/api/publico

Desafios para Expandir

  1. Rate limits diferenciados – Configure limites diferentes por rota: rotas leves com 100 req/min e rotas pesadas com 10 req/min, usando configuracoes diferentes por grupo de rotas.

  2. Sliding window – Implemente o algoritmo sliding window log como alternativa ao token bucket, que oferece contagem mais precisa dentro de janelas de tempo moveis.

  3. Persistencia com Redis – Substitua o HashMap em memoria por Redis usando a crate redis, permitindo que o rate limiter funcione em ambientes com multiplas instancias do servidor.

  4. Dashboard de monitoramento – Crie um endpoint /admin/metricas que mostra estatisticas em tempo real: IPs mais ativos, total de requisicoes bloqueadas e distribuicao de uso.

  5. Rate limit por chave de API – Alem de limitar por IP, adicione suporte a chaves de API com limites customizados por cliente, onde clientes premium tem limites mais altos.

Veja Tambem