{
  "id": "rust",
  "title": "Token bucket rate limiter with Redis and Rust",
  "url": "https://redis.io/docs/latest/develop/use-cases/rate-limiter/rust/",
  "summary": "Implement a token bucket rate limiter using Redis and Lua scripts in Rust",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc"
  ],
  "last_updated": "2026-04-16T13:29:55-07:00",
  "children": [],
  "page_type": "content",
  "content_hash": "a7496136cfea5e5aee6d97376b2f6d423ce728402717252fb58e9e4c3715a043",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "This guide shows you how to implement a distributed token bucket rate limiter using Redis and Lua scripts in Rust with the [`redis-rs`](https://redis.io/docs/latest/develop/clients/rust) client library."
    },
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Rate limiting is a critical technique for controlling the rate at which operations are performed. Common use cases include:\n\n* Limiting API requests per user or IP address\n* Preventing abuse and protecting against denial-of-service attacks\n* Ensuring fair resource allocation across multiple clients\n* Throttling background jobs or batch operations\n\nThe **token bucket algorithm** is a popular rate limiting approach that allows bursts of traffic while maintaining an average rate limit over time. This guide covers the Rust implementation using the [`redis-rs`](https://redis.io/docs/latest/develop/clients/rust) client library, taking advantage of Rust's type safety, ownership model, and zero-cost abstractions."
    },
    {
      "id": "how-it-works",
      "title": "How it works",
      "role": "content",
      "text": "The token bucket algorithm works like a bucket that holds tokens:\n\n1. **Initialization**: The bucket starts with a maximum capacity of tokens\n2. **Refill**: Tokens are added to the bucket at a constant rate (for example, 1 token per second)\n3. **Consumption**: Each request consumes one token from the bucket\n4. **Decision**: If tokens are available, the request is allowed; otherwise, it's denied\n5. **Capacity limit**: The bucket never exceeds its maximum capacity\n\nThis approach allows for burst traffic (using accumulated tokens) while enforcing an average rate limit over time."
    },
    {
      "id": "why-use-redis",
      "title": "Why use Redis?",
      "role": "content",
      "text": "Redis is ideal for distributed rate limiting because:\n\n* **Atomic operations**: Lua scripts execute atomically, preventing race conditions\n* **Shared state**: Multiple application servers can share the same rate limit counters\n* **High performance**: In-memory operations provide microsecond latency\n* **Automatic expiration**: Keys can be set to expire automatically (though not used in this implementation)"
    },
    {
      "id": "the-lua-script",
      "title": "The Lua script",
      "role": "content",
      "text": "The core of this implementation is a Lua script that runs atomically on the Redis server. This ensures that checking and updating the token bucket happens in a single operation, preventing race conditions in distributed environments.\n\nHere's how the script works:\n\n[code example]"
    },
    {
      "id": "script-breakdown",
      "title": "Script breakdown",
      "role": "content",
      "text": "1. **State retrieval**: Uses [`HMGET`](https://redis.io/docs/latest/commands/hmget) to fetch the current token count and last refill time from a hash\n2. **Initialization**: On first use, sets tokens to full capacity\n3. **Token refill calculation**: Computes how many tokens should be added based on elapsed time\n4. **Capacity enforcement**: Uses `math.min()` to ensure tokens never exceed capacity\n5. **Token consumption**: Decrements the token count if available\n6. **State update**: Uses [`HMSET`](https://redis.io/docs/latest/commands/hmset) to save the new state\n7. **Return value**: Returns both the decision (allowed/denied) and remaining tokens"
    },
    {
      "id": "why-atomicity-matters",
      "title": "Why atomicity matters",
      "role": "content",
      "text": "Without atomic execution, race conditions could occur:\n\n* **Double spending**: Two requests could read the same token count and both succeed when only one should\n* **Lost updates**: Concurrent updates could overwrite each other's changes\n* **Inconsistent state**: Token count and refill time could become desynchronized\n\nUsing [`EVAL`](https://redis.io/docs/latest/commands/eval) or [`EVALSHA`](https://redis.io/docs/latest/commands/evalsha) ensures the entire operation executes atomically, making it safe for distributed systems."
    },
    {
      "id": "installation",
      "title": "Installation",
      "role": "setup",
      "text": "Add the `redis` crate to your `Cargo.toml`:\n\n[code example]\n\nFor async support, enable the `tokio-comp` or `async-std-comp` feature:\n\n[code example]"
    },
    {
      "id": "using-the-rust-module",
      "title": "Using the Rust module",
      "role": "content",
      "text": "The `TokenBucket` struct provides a type-safe interface for rate limiting\n([source](token_bucket.rs)):\n\n[code example]\n\nRust's `Result` type provides compile-time error handling, ensuring all Redis errors are handled explicitly. The `TokenBucket` struct owns its configuration, leveraging Rust's ownership model to prevent data races."
    },
    {
      "id": "configuration-parameters",
      "title": "Configuration parameters",
      "role": "configuration",
      "text": "* **capacity**: Maximum number of tokens in the bucket (controls burst size)\n* **refill_rate**: Number of tokens added per refill interval (as `f64`)\n* **refill_interval**: Time in seconds between refills (as `f64`)\n\nFor example:\n* `capacity: 10, refill_rate: 1.0, refill_interval: 1.0` allows 10 requests per second with bursts up to 10\n* `capacity: 100, refill_rate: 10.0, refill_interval: 1.0` allows 10 requests per second with bursts up to 100\n* `capacity: 60, refill_rate: 1.0, refill_interval: 60.0` allows 1 request per minute with bursts up to 60"
    },
    {
      "id": "rate-limit-keys",
      "title": "Rate limit keys",
      "role": "content",
      "text": "The `key` parameter identifies what you're rate limiting. Common patterns:\n\n* **Per user**: `user:{user_id}` - Limit each user independently\n* **Per IP address**: `ip:{ip_address}` - Limit by client IP\n* **Per API endpoint**: `api:{endpoint}:{user_id}` - Different limits per endpoint\n* **Global**: `global:api` - Single limit shared across all requests"
    },
    {
      "id": "script-caching-with-evalsha",
      "title": "Script caching with EVALSHA",
      "role": "content",
      "text": "The Rust implementation uses [`EVALSHA`](https://redis.io/docs/latest/commands/evalsha) for optimal performance. The script is loaded once with `SCRIPT LOAD`, and subsequent calls use the cached SHA1 hash. If the script is evicted, the module automatically falls back to [`EVAL`](https://redis.io/docs/latest/commands/eval) and reloads it:\n\n[code example]"
    },
    {
      "id": "thread-safety-with-arc-and-mutex",
      "title": "Thread safety with Arc and Mutex",
      "role": "content",
      "text": "For concurrent access across threads, wrap the connection in `Arc<Mutex<>>`:\n\n[code example]"
    },
    {
      "id": "running-the-demo",
      "title": "Running the demo",
      "role": "content",
      "text": "A demonstration HTTP server is included to show the rate limiter in action\n([source](demo_server.rs)):\n\n[code example]\n\nThe demo provides an interactive web interface where you can:\n\n* Submit requests and see them allowed or denied in real-time\n* View the current token count\n* Adjust rate limit parameters dynamically\n* Test different rate limiting scenarios\n\nThe demo assumes Redis is running on `localhost:6379` but you can specify a different host using the `REDIS_URL` environment variable. Visit `http://localhost:8080` in your browser to try it out."
    },
    {
      "id": "response-headers",
      "title": "Response headers",
      "role": "returns",
      "text": "It's common to include rate limit information in HTTP response headers. Here's an example using a hypothetical web framework:\n\n[code example]"
    },
    {
      "id": "customization",
      "title": "Customization",
      "role": "content",
      "text": ""
    },
    {
      "id": "using-with-async-await",
      "title": "Using with async/await",
      "role": "content",
      "text": "For async web frameworks like `actix-web` or `axum`, use the async version of the Redis client:\n\n[code example]"
    },
    {
      "id": "error-handling-strategies",
      "title": "Error handling strategies",
      "role": "errors",
      "text": "The `allow` method returns a `RedisResult`, which you should handle based on your requirements. Decide whether to fail open (allow requests) or fail closed (deny requests) when Redis is unavailable:\n\n[code example]"
    },
    {
      "id": "using-with-connection-pools",
      "title": "Using with connection pools",
      "role": "content",
      "text": "For production use, maintain a connection pool rather than creating connections per request:\n\n[code example]"
    },
    {
      "id": "custom-token-consumption",
      "title": "Custom token consumption",
      "role": "content",
      "text": "To consume multiple tokens per request, modify the Lua script or call `allow` multiple times:\n\n[code example]"
    },
    {
      "id": "learn-more",
      "title": "Learn more",
      "role": "related",
      "text": "* [EVAL command](https://redis.io/docs/latest/commands/eval) - Execute Lua scripts\n* [EVALSHA command](https://redis.io/docs/latest/commands/evalsha) - Execute cached Lua scripts\n* [Lua scripting](https://redis.io/docs/latest/develop/programmability/eval-intro) - Introduction to Redis Lua scripting\n* [HMGET command](https://redis.io/docs/latest/commands/hmget) - Get multiple hash fields\n* [HMSET command](https://redis.io/docs/latest/commands/hmset) - Set multiple hash fields\n* [Rust client](https://redis.io/docs/latest/develop/clients/rust) - Redis Rust client documentation"
    }
  ],
  "examples": [
    {
      "id": "the-lua-script-ex0",
      "language": "lua",
      "code": "local key = KEYS[1]\nlocal capacity = tonumber(ARGV[1])\nlocal refill_rate = tonumber(ARGV[2])\nlocal refill_interval = tonumber(ARGV[3])\nlocal now = tonumber(ARGV[4])\n\n-- Get current state or initialize\nlocal bucket = redis.call('HMGET', key, 'tokens', 'last_refill')\nlocal tokens = tonumber(bucket[1])\nlocal last_refill = tonumber(bucket[2])\n\n-- Initialize if this is the first request\nif tokens == nil then\n    tokens = capacity\n    last_refill = now\nend\n\n-- Calculate token refill\nlocal time_passed = now - last_refill\nlocal refills = math.floor(time_passed / refill_interval)\n\nif refills > 0 then\n    tokens = math.min(capacity, tokens + (refills * refill_rate))\n    last_refill = last_refill + (refills * refill_interval)\nend\n\n-- Try to consume a token\nlocal allowed = 0\nif tokens >= 1 then\n    tokens = tokens - 1\n    allowed = 1\nend\n\n-- Update state\nredis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)\n\n-- Return result: allowed (1 or 0) and remaining tokens\nreturn {allowed, tokens}",
      "section_id": "the-lua-script"
    },
    {
      "id": "installation-ex0",
      "language": "toml",
      "code": "[dependencies]\nredis = \"0.24\"",
      "section_id": "installation"
    },
    {
      "id": "installation-ex1",
      "language": "toml",
      "code": "[dependencies]\nredis = { version = \"0.24\", features = [\"tokio-comp\"] }\ntokio = { version = \"1\", features = [\"full\"] }",
      "section_id": "installation"
    },
    {
      "id": "using-the-rust-module-ex0",
      "language": "rust",
      "code": "use redis::{Client, Commands, RedisResult};\n\nfn main() -> RedisResult<()> {\n    // Create a Redis connection\n    let client = Client::open(\"redis://localhost:6379/\")?;\n    let mut con = client.get_connection()?;\n\n    // Create a rate limiter: 10 requests per second\n    let limiter = TokenBucket::new(\n        10,   // capacity: Maximum burst size\n        1.0,  // refill_rate: Add 1 token per interval\n        1.0,  // refill_interval: Every 1 second\n    );\n\n    // Check if a request should be allowed\n    let result = limiter.allow(&mut con, \"user:123\")?;\n\n    if result.allowed {\n        println!(\"Request allowed. {} tokens remaining.\", result.remaining);\n        // Process the request\n    } else {\n        println!(\"Request denied. Rate limit exceeded.\");\n        // Return 429 Too Many Requests\n    }\n\n    Ok(())\n}",
      "section_id": "using-the-rust-module"
    },
    {
      "id": "script-caching-with-evalsha-ex0",
      "language": "rust",
      "code": "// The module handles script caching automatically.\n// First call loads the script, subsequent calls use EVALSHA.\nlet result1 = limiter.allow(&mut con, \"user:123\")?; // Uses EVAL + caches\nlet result2 = limiter.allow(&mut con, \"user:123\")?; // Uses EVALSHA (faster)",
      "section_id": "script-caching-with-evalsha"
    },
    {
      "id": "thread-safety-with-arc-and-mutex-ex0",
      "language": "rust",
      "code": "use std::sync::{Arc, Mutex};\nuse std::thread;\n\nlet client = Client::open(\"redis://localhost:6379/\")?;\nlet connection = Arc::new(Mutex::new(client.get_connection()?));\n\nlet limiter = TokenBucket::new(10, 1.0, 1.0);\n\nlet handles: Vec<_> = (0..10)\n    .map(|i| {\n        let con = Arc::clone(&connection);\n        let limiter = limiter.clone();\n        thread::spawn(move || {\n            let mut con = con.lock().unwrap();\n            match limiter.allow(&mut *con, \"shared:resource\") {\n                Ok(result) => {\n                    println!(\"Thread {}: allowed={}, remaining={}\",\n                             i, result.allowed, result.remaining);\n                }\n                Err(e) => eprintln!(\"Thread {}: error: {}\", i, e),\n            }\n        })\n    })\n    .collect();\n\nfor handle in handles {\n    handle.join().unwrap();\n}",
      "section_id": "thread-safety-with-arc-and-mutex"
    },
    {
      "id": "running-the-demo-ex0",
      "language": "bash",
      "code": "# Install dependencies\ncargo build\n\n# Run the demo server\ncargo run --bin demo",
      "section_id": "running-the-demo"
    },
    {
      "id": "response-headers-ex0",
      "language": "rust",
      "code": "use std::time::{SystemTime, UNIX_EPOCH};\n\nfn handle_request(\n    limiter: &TokenBucket,\n    con: &mut Connection,\n    user_id: &str,\n) -> Result<Response, Error> {\n    let result = limiter.allow(con, &format!(\"user:{}\", user_id))?;\n\n    let mut response = Response::new();\n\n    // Add standard rate limit headers\n    response.headers.insert(\n        \"X-RateLimit-Limit\",\n        limiter.capacity.to_string(),\n    );\n    response.headers.insert(\n        \"X-RateLimit-Remaining\",\n        result.remaining.floor().to_string(),\n    );\n\n    let reset_time = SystemTime::now()\n        .duration_since(UNIX_EPOCH)\n        .unwrap()\n        .as_secs() + limiter.refill_interval as u64;\n    response.headers.insert(\n        \"X-RateLimit-Reset\",\n        reset_time.to_string(),\n    );\n\n    if !result.allowed {\n        response.status = 429; // Too Many Requests\n        response.headers.insert(\n            \"Retry-After\",\n            limiter.refill_interval.floor().to_string(),\n        );\n        return Ok(response);\n    }\n\n    // Process the request\n    Ok(response)\n}",
      "section_id": "response-headers"
    },
    {
      "id": "using-with-async-await-ex0",
      "language": "rust",
      "code": "use redis::aio::Connection;\n\nasync fn rate_limit_check(\n    limiter: &TokenBucket,\n    con: &mut Connection,\n    key: &str,\n) -> redis::RedisResult<RateLimitResult> {\n    limiter.allow_async(con, key).await\n}\n\n// Example with axum\nuse axum::{\n    extract::State,\n    http::StatusCode,\n    response::IntoResponse,\n};\n\nasync fn handle_request(\n    State(app_state): State<AppState>,\n) -> impl IntoResponse {\n    let mut con = app_state.redis_pool.get().await.unwrap();\n\n    match app_state.limiter.allow_async(&mut con, \"user:123\").await {\n        Ok(result) if result.allowed => {\n            (StatusCode::OK, \"Request processed\")\n        }\n        Ok(_) => {\n            (StatusCode::TOO_MANY_REQUESTS, \"Rate limit exceeded\")\n        }\n        Err(e) => {\n            eprintln!(\"Redis error: {}\", e);\n            (StatusCode::INTERNAL_SERVER_ERROR, \"Service unavailable\")\n        }\n    }\n}",
      "section_id": "using-with-async-await"
    },
    {
      "id": "error-handling-strategies-ex0",
      "language": "rust",
      "code": "// Fail open: allow requests when Redis is unavailable\nlet allowed = match limiter.allow(&mut con, \"user:123\") {\n    Ok(result) => result.allowed,\n    Err(e) => {\n        eprintln!(\"Rate limiter error: {}. Failing open.\", e);\n        true // Allow the request\n    }\n};\n\n// Or fail closed: deny requests when Redis is unavailable\nlet allowed = match limiter.allow(&mut con, \"user:123\") {\n    Ok(result) => result.allowed,\n    Err(e) => {\n        eprintln!(\"Rate limiter error: {}. Failing closed.\", e);\n        false // Deny the request\n    }\n};",
      "section_id": "error-handling-strategies"
    },
    {
      "id": "using-with-connection-pools-ex0",
      "language": "rust",
      "code": "use r2d2_redis::{r2d2, RedisConnectionManager};\n\n// Create a connection pool\nlet manager = RedisConnectionManager::new(\"redis://localhost:6379\")?;\nlet pool = r2d2::Pool::builder()\n    .max_size(15)\n    .build(manager)?;\n\nlet limiter = TokenBucket::new(10, 1.0, 1.0);\n\n// Use the pool\nlet mut con = pool.get()?;\nlet result = limiter.allow(&mut *con, \"user:123\")?;",
      "section_id": "using-with-connection-pools"
    },
    {
      "id": "custom-token-consumption-ex0",
      "language": "rust",
      "code": "// Reserve 5 tokens for a batch operation\nlet mut tokens_acquired = 0;\nfor _ in 0..5 {\n    match limiter.allow(&mut con, \"batch:operation\")? {\n        result if result.allowed => tokens_acquired += 1,\n        _ => break,\n    }\n}\n\nif tokens_acquired == 5 {\n    println!(\"Batch operation allowed\");\n} else {\n    println!(\"Not enough tokens. Acquired: {}\", tokens_acquired);\n}",
      "section_id": "custom-token-consumption"
    }
  ]
}
