rok-rate-limit 0.3.0

Rate limiting Tower middleware and programmatic Limiter API for the rok ecosystem
Documentation

rok-rate-limit

Rate limiting middleware for Axum with in-memory and Redis backends, 429 responses, and standard headers.

Part of the Rok Framework — a full-stack Rust web framework built on Axum 0.8 and SQLx 0.8.

crates.io docs.rs MIT

Features

  • Sliding window algorithm — smooth rate limit without burst spikes at window boundaries
  • In-memory backend using DashMap — zero-dependency, suitable for single-process deployments
  • Redis backend — distributed rate limiting across multiple server processes
  • Pluggable key extraction — per-IP, per-authenticated-user, per-API-key, or custom closure
  • Standard rate limit response headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset)
  • Retry-After header on 429 responses so clients know when to retry
  • Per-route granularity — apply different limits to login vs. API endpoints
  • RFC 6585 compliant 429 Too Many Requests responses with JSON body

Installation

[dependencies]
rok-rate-limit = "0.2"

Redis backend:

rok-rate-limit = { version = "0.2", features = ["redis"] }

Quick Start

use std::time::Duration;
use axum::{Router, routing::{get, post}};
use rok_rate_limit::{RateLimitLayer, KeyExtractor};

// 100 requests per 60 seconds, keyed by client IP
let global_limiter = RateLimitLayer::new(100, Duration::from_secs(60))
    .key(KeyExtractor::ip());

// Tighter limit on the login endpoint to slow brute-force attempts
let login_limiter = RateLimitLayer::new(5, Duration::from_secs(60))
    .key(KeyExtractor::ip());

let app = Router::new()
    .route("/api/users", get(users_index))
    .layer(global_limiter)
    .route("/auth/login", post(login))
    .layer(login_limiter);

Core API

RateLimitLayer

impl RateLimitLayer {
    /// Create an in-memory limiter: `max_requests` allowed per `window`.
    pub fn new(max_requests: u64, window: Duration) -> Self;

    /// Create a Redis-backed limiter (requires `redis` feature).
    pub fn redis(url: &str, max_requests: u64, window: Duration) -> Self;

    /// Set the key extraction strategy.
    pub fn key(self, extractor: KeyExtractor) -> Self;

    /// Customize the 429 response body.
    pub fn on_limit_exceeded(self, handler: impl Fn() -> Response + Send + Sync + 'static) -> Self;
}

KeyExtractor

// Rate-limit by remote IP address (X-Forwarded-For aware)
KeyExtractor::ip()

// Rate-limit by the value of a specific header
KeyExtractor::header("X-API-Key")

// Rate-limit by the authenticated user's JWT `sub` claim
KeyExtractor::jwt_sub()

// Rate-limit by any custom function over the request
KeyExtractor::custom(|req: &Request| -> String {
    req.headers()
        .get("X-Tenant-Id")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("anonymous")
        .to_string()
})

Response Headers

X-RateLimit-Limit:     100
X-RateLimit-Remaining: 42
X-RateLimit-Reset:     1716912060   (Unix timestamp when the window resets)
Retry-After:           18           (seconds; only present on 429 responses)

Feature Flags

Flag Description Default
redis Redis backend via fred async client disabled
memory DashMap in-memory backend enabled

Integration

rok-rate-limit pairs naturally with rok-auth to protect authentication endpoints. Mount a strict limiter on auth routes and a more generous one on API routes:

use rok_auth::AuthLayer;
use rok_rate_limit::{RateLimitLayer, KeyExtractor};

// Authenticated routes: rate-limit per user, not per IP
let api_limiter = RateLimitLayer::new(1000, Duration::from_secs(3600))
    .key(KeyExtractor::jwt_sub());

let app = Router::new()
    .nest("/api", api_router().layer(api_limiter))
    .layer(AuthLayer::jwt(jwt_config));

For distributed deployments, switch to the Redis backend via RateLimitLayer::redis(...). The Redis backend uses Lua scripts for atomic sliding-window increments, so limits are accurate across any number of server instances.

License

MIT