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
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.
Sliding window – Implemente o algoritmo sliding window log como alternativa ao token bucket, que oferece contagem mais precisa dentro de janelas de tempo moveis.
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.Dashboard de monitoramento – Crie um endpoint
/admin/metricasque mostra estatisticas em tempo real: IPs mais ativos, total de requisicoes bloqueadas e distribuicao de uso.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
- HashMap na Biblioteca Padrao – Armazenamento dos buckets por IP
- Mutex para Exclusao Mutua – Protecao do estado compartilhado
- Modulo time – Medicao de tempo para o token bucket
- Axum vs Actix: Qual Framework Escolher? – Comparativo de frameworks web
- Rust para Microsservicos – Arquitetura de microsservicos em Rust