route-ratelimit

Crates.ioroute-ratelimit
lib.rsroute-ratelimit
version0.1.0
created_at2025-12-26 05:04:52.554864+00
updated_at2025-12-26 05:04:52.554864+00
descriptionRoute-based rate limiting middleware for reqwest
homepagehttps://github.com/haut/route-ratelimit
repositoryhttps://github.com/haut/route-ratelimit
max_upload_size
id2005269
size100,323
Richard Haut (Haut)

documentation

https://docs.rs/route-ratelimit

README

route-ratelimit

Crates.io Documentation License: MIT CI MSRV

Route-based rate limiting middleware for reqwest.

Features

  • Endpoint matching: Match requests by host, HTTP method, and path prefix
  • Multiple rate limits: Stack burst and sustained limits on the same endpoint
  • Configurable behavior: Choose to delay requests or return errors per endpoint
  • Lock-free performance: Uses GCRA algorithm with atomic operations
  • Shared state: Rate limits are tracked across all client clones

Installation

Add to your Cargo.toml:

[dependencies]
route-ratelimit = "0.1"
reqwest = "0.12"
reqwest-middleware = "0.4"

Quick Start

use route_ratelimit::RateLimitMiddleware;
use reqwest_middleware::ClientBuilder;
use std::time::Duration;

#[tokio::main]
async fn main() {
    let middleware = RateLimitMiddleware::builder()
        .route(|r| r.limit(100, Duration::from_secs(10)))
        .build();

    let client = ClientBuilder::new(reqwest::Client::new())
        .with(middleware)
        .build();

    // Requests are automatically rate-limited
    client.get("https://api.example.com/data").send().await.unwrap();
}

Usage

Host-Scoped Routes

Organize rate limits by host for cleaner configuration:

use route_ratelimit::RateLimitMiddleware;
use std::time::Duration;
use http::Method;

let middleware = RateLimitMiddleware::builder()
    .host("api.example.com", |host| {
        host
            // General limit for all endpoints on this host
            .route(|r| r.limit(9000, Duration::from_secs(10)))
            // Specific limit for /book endpoints (both limits apply)
            .route(|r| r.path("/book").limit(1500, Duration::from_secs(10)))
            // Method + path specific limits
            .route(|r| {
                r.method(Method::POST)
                    .path("/order")
                    .limit(3500, Duration::from_secs(10))   // Burst
                    .limit(36000, Duration::from_secs(600)) // Sustained
            })
    })
    .build();

Multiple Limits (Burst + Sustained)

Apply both burst and sustained limits to the same endpoint:

use route_ratelimit::RateLimitMiddleware;
use std::time::Duration;

let middleware = RateLimitMiddleware::builder()
    .route(|r| {
        r.path("/api")
            .limit(100, Duration::from_secs(10))   // Burst: 100 req/10s
            .limit(1000, Duration::from_secs(600)) // Sustained: 1000 req/10min
    })
    .build();

Error Behavior

By default, requests are delayed until they can proceed. Use ThrottleBehavior::Error to fail fast:

use route_ratelimit::{RateLimitMiddleware, ThrottleBehavior};
use std::time::Duration;

let middleware = RateLimitMiddleware::builder()
    .route(|r| {
        r.limit(10, Duration::from_secs(1))
            .on_limit(ThrottleBehavior::Error) // Return error immediately
    })
    .build();

Route Matching

All Matching Routes Apply

Routes are checked in order, and all matching routes' limits are applied. This allows layering general limits with specific ones:

use route_ratelimit::RateLimitMiddleware;
use std::time::Duration;

let middleware = RateLimitMiddleware::builder()
    .host("api.example.com", |host| {
        host
            // This applies to ALL requests to api.example.com
            .route(|r| r.limit(9000, Duration::from_secs(10)))
            // This ALSO applies to /book requests (both limits enforced)
            .route(|r| r.path("/book").limit(1500, Duration::from_secs(10)))
    })
    .build();

Host Matching

Host matching uses only the hostname, excluding the port:

// Matches: https://api.example.com/path
// Matches: https://api.example.com:8443/path
// Does NOT match: https://other.example.com/path
.host("api.example.com", |h| h.route(|r| r.limit(100, Duration::from_secs(10))))

Path Matching

Path matching uses segment boundaries, not simple prefix matching:

Pattern Matches Does NOT Match
/order /order, /order/, /order/123 /orders, /order-test
/api/v1 /api/v1/users, /api/v1/ /api/v2, /api/v10

Optional Features

Tracing Support

Enable the tracing feature for diagnostic logging:

[dependencies]
route-ratelimit = { version = "0.1", features = ["tracing"] }

This enables warnings for potentially problematic configurations (e.g., catch-all routes preceding specific routes).

Memory Management

For long-running applications, periodically clean up stale rate limit state:

use route_ratelimit::RateLimitMiddleware;
use std::time::Duration;

let middleware = RateLimitMiddleware::builder()
    .route(|r| r.limit(100, Duration::from_secs(10)))
    .build();

// Call periodically (e.g., every hour)
middleware.cleanup();

// Monitor state size
println!("Active rate limit entries: {}", middleware.state_count());

Examples

See the examples directory for complete usage examples:

  • Polymarket API - Complete rate limit configuration for a real-world API

Minimum Supported Rust Version

This crate requires Rust 1.88.0 or later.

License

Licensed under the MIT License. See LICENSE for details.

Commit count: 0

cargo fmt