Rate Limiting Guide
Rate limiting protects your API from abuse and ensures fair usage across all clients.
The armature-ratelimit crate provides a comprehensive, production-ready rate limiting solution.
Table of Contents
- Overview
- Features
- Quick Start
- Algorithms
- Storage Backends
- Key Extraction
- Middleware Integration
- Configuration
- Best Practices
- Common Pitfalls
- API Reference
Overview
Rate limiting controls how many requests a client can make within a time window. This prevents:
- Abuse: Malicious actors overwhelming your API
- Resource exhaustion: A single client consuming all server resources
- Cascading failures: Overload propagating through your system
- Cost overruns: Excessive usage driving up infrastructure costs
Features
- âś… Multiple Algorithms: Token bucket, sliding window log, fixed window
- âś… Distributed Support: Redis backend for multi-instance deployments
- âś… Flexible Key Extraction: By IP, user ID, API key, or custom function
- âś… Standard Headers:
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset - âś… Per-Route Limits: Different limits for different endpoints
- âś… Bypass Rules: Whitelist specific clients or API keys
- âś… Fail-Open Mode: Continue serving requests if rate limit storage fails
Quick Start
Add the dependency:
[dependencies]
armature-ratelimit = "0.1"
Basic usage:
use armature_ratelimit::{RateLimiter, Algorithm};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a rate limiter with token bucket algorithm
let limiter = RateLimiter::builder()
.algorithm(Algorithm::TokenBucket {
capacity: 100, // Maximum burst size
refill_rate: 10.0, // 10 tokens per second
})
.build()
.await?;
// Check if a request is allowed
let result = limiter.check("client_ip_123").await?;
if result.allowed {
println!("Request allowed! {} remaining", result.remaining);
} else {
println!("Rate limited. Retry after {:?}", result.retry_after);
}
Ok(())
}
Algorithms
Token Bucket
The token bucket algorithm provides smooth rate limiting with burst capacity.
How it works:
- A bucket starts full with
capacitytokens - Each request consumes one token
- Tokens are refilled at
refill_rateper second - Requests are denied when the bucket is empty
Best for: APIs that allow occasional bursts but need average rate control.
use armature_ratelimit::Algorithm;
let algo = Algorithm::TokenBucket {
capacity: 100, // Allow bursts up to 100 requests
refill_rate: 10.0, // Steady rate of 10 requests/second
};
Example scenario: A user can make up to 100 rapid requests, then must wait for tokens to refill at 10/second.
Sliding Window Log
The sliding window log algorithm provides precise rate limiting by tracking individual request timestamps.
How it works:
- Each request timestamp is logged
- On each request, count timestamps within the window
- Deny if count exceeds
max_requests - Old timestamps are automatically cleaned up
Best for: Strict rate limiting where accuracy is critical.
use armature_ratelimit::Algorithm;
use std::time::Duration;
let algo = Algorithm::SlidingWindowLog {
max_requests: 100, // 100 requests...
window: Duration::from_secs(60), // ...per minute
};
Trade-offs:
- âś… Most accurate algorithm
- âś… No boundary issues
- ❌ Higher memory usage (stores all timestamps)
Fixed Window
The fixed window algorithm divides time into fixed intervals and counts requests per window.
How it works:
- Time is divided into fixed windows (e.g., every minute)
- Each request increments the counter for the current window
- Counter resets when a new window starts
- Deny if counter exceeds
max_requests
Best for: Simple use cases, lowest resource usage.
use armature_ratelimit::Algorithm;
use std::time::Duration;
let algo = Algorithm::FixedWindow {
max_requests: 100,
window: Duration::from_secs(60),
};
Trade-offs:
- âś… Simple and efficient
- âś… Lowest memory usage
- ❌ Boundary burst issue: clients can make 2x requests at window boundaries
Algorithm Comparison
| Algorithm | Accuracy | Memory | Complexity | Burst Handling |
|---|---|---|---|---|
| Token Bucket | Medium | Low | Low | Controlled bursts |
| Sliding Window | High | Medium | Medium | No bursts |
| Fixed Window | Low | Very Low | Very Low | Boundary bursts |
Storage Backends
In-Memory Store (Default)
Uses DashMap for thread-safe concurrent access. Suitable for single-instance deployments or development.
let limiter = RateLimiter::builder()
.algorithm(Algorithm::token_bucket_default())
.memory_store() // This is the default
.build()
.await?;
Pros:
- Zero latency
- No external dependencies
- Simple setup
Cons:
- Not shared across instances
- State lost on restart
Redis Store
Uses Redis for distributed rate limiting. Required for multi-instance deployments.
let limiter = RateLimiter::builder()
.algorithm(Algorithm::token_bucket_default())
.redis_store("redis://localhost:6379")
.build()
.await?;
Pros:
- Shared across all instances
- Persistent state
- Atomic operations via Lua scripts
Cons:
- Network latency
- Requires Redis infrastructure
Enable the feature:
[dependencies]
armature-ratelimit = { version = "0.1", features = ["redis"] }
Key Extraction
Rate limits are applied per-key. The key extraction strategy determines how clients are identified.
By IP Address (Default)
use armature_ratelimit::{RateLimitMiddleware, KeyExtractor};
let middleware = RateLimitMiddleware::new(limiter)
.with_extractor(KeyExtractor::Ip);
By User ID
Requires authentication. Falls back to IP if user is not authenticated.
let middleware = RateLimitMiddleware::new(limiter)
.with_extractor(KeyExtractor::UserId);
By API Key
Extracts the key from a header (e.g., X-API-Key).
let middleware = RateLimitMiddleware::new(limiter)
.with_extractor(KeyExtractor::ApiKey {
header_name: "X-API-Key".to_string(),
});
By IP and Path
Different limits per endpoint.
let middleware = RateLimitMiddleware::new(limiter)
.with_extractor(KeyExtractor::IpAndPath);
This creates keys like 192.168.1.1:/api/users, allowing different rate limits
for different endpoints.
Custom Extractor
Build complex extraction logic:
use armature_ratelimit::extractor::KeyExtractorBuilder;
let extractor = KeyExtractorBuilder::new()
.prefer_user_id() // Try user ID first
.prefer_api_key("X-API-Key") // Then API key
// Falls back to IP automatically
.build();
Middleware Integration
Basic Middleware
use armature_ratelimit::{RateLimiter, RateLimitMiddleware, Algorithm};
use std::sync::Arc;
let limiter = Arc::new(
RateLimiter::builder()
.token_bucket(100, 10.0)
.build()
.await?
);
let middleware = RateLimitMiddleware::new(limiter)
.with_headers(true) // Include X-RateLimit-* headers
.with_error_message("Too many requests. Please slow down.");
Checking Requests
use armature_ratelimit::extractor::RequestInfo;
use std::net::{IpAddr, Ipv4Addr};
// Extract request info from your HTTP framework
let info = RequestInfo::new("/api/users", "GET")
.with_ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))
.with_user_id("user_123");
// Check rate limit
let response = middleware.check(&info).await;
match response {
RateLimitCheckResponse::Allowed { headers } => {
// Add headers to response and continue
if let Some(h) = headers {
// Add X-RateLimit-Limit, X-RateLimit-Remaining, etc.
}
}
RateLimitCheckResponse::Limited { headers, message, retry_after } => {
// Return 429 Too Many Requests
// Include Retry-After header
}
}
Configuration
Builder Options
let limiter = RateLimiter::builder()
// Algorithm (required)
.algorithm(Algorithm::TokenBucket {
capacity: 100,
refill_rate: 10.0,
})
// Or use convenience methods
.token_bucket(100, 10.0)
.sliding_window(100, Duration::from_secs(60))
.fixed_window(100, Duration::from_secs(60))
// Storage backend
.memory_store()
.redis_store("redis://localhost:6379")
// Key prefix for storage
.key_prefix("api:ratelimit")
// Include headers in responses
.include_headers(true)
// Fail open on storage errors
.skip_on_error(true)
// Custom error message
.error_message("Rate limit exceeded")
// Bypass specific keys
.bypass_key("admin_api_key")
.bypass_keys(["internal_service", "monitoring"])
.build()
.await?;
Response Headers
When enabled, these headers are included in responses:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed |
X-RateLimit-Remaining |
Remaining requests in current window |
X-RateLimit-Reset |
Unix timestamp when the limit resets |
Retry-After |
Seconds until the client can retry (only on 429) |
Best Practices
1. Choose the Right Algorithm
- Token Bucket: Most APIs—allows bursts, smooth average rate
- Sliding Window: Financial/gaming APIs—strict, no burst exploitation
- Fixed Window: High-volume, latency-sensitive—simple and fast
2. Use Redis for Production
// Single instance: memory is fine
let limiter = RateLimiter::builder()
.token_bucket(100, 10.0)
.build()
.await?;
// Multiple instances: use Redis
let limiter = RateLimiter::builder()
.token_bucket(100, 10.0)
.redis_store("redis://redis-cluster:6379")
.build()
.await?;
3. Implement Tiered Limits
// Different limits for different user tiers
async fn check_rate_limit(user: &User, limiter: &RateLimiter) -> bool {
let key = match user.tier {
Tier::Free => format!("free:{}", user.id),
Tier::Pro => format!("pro:{}", user.id),
Tier::Enterprise => return true, // No limit
};
limiter.check(&key).await.map(|r| r.allowed).unwrap_or(true)
}
4. Include Helpful Headers
Always include rate limit headers so clients can self-regulate:
let middleware = RateLimitMiddleware::new(limiter)
.with_headers(true);
5. Use Bypass for Internal Services
let limiter = RateLimiter::builder()
.token_bucket(100, 10.0)
.bypass_key("internal_service_key")
.bypass_key("health_check_key")
.build()
.await?;
Common Pitfalls
❌ Don't: Use IP-only limiting behind a proxy
// All requests will have the same IP (the proxy's IP)
let middleware = RateLimitMiddleware::new(limiter)
.with_extractor(KeyExtractor::Ip);
âś… Do: Use X-Forwarded-For or X-Real-IP
// Extract the real client IP from headers
let info = RequestInfo::new(path, method)
.with_header("X-Forwarded-For", forwarded_for);
// Or use the first IP from X-Forwarded-For
fn get_real_ip(headers: &Headers) -> Option<IpAddr> {
headers.get("X-Forwarded-For")
.and_then(|h| h.split(',').next())
.and_then(|ip| ip.trim().parse().ok())
}
❌ Don't: Fail closed on errors
// If Redis is down, all requests will be denied!
let limiter = RateLimiter::builder()
.skip_on_error(false) // Bad for availability
.build()
.await?;
âś… Do: Fail open (default)
let limiter = RateLimiter::builder()
.skip_on_error(true) // Default, allows requests on storage failure
.build()
.await?;
❌ Don't: Use fixed window for strict limits
// Client can make 200 requests in 2 seconds by timing window boundaries
let algo = Algorithm::FixedWindow {
max_requests: 100,
window: Duration::from_secs(60),
};
âś… Do: Use sliding window for strict limits
// Accurate limiting, no boundary exploitation
let algo = Algorithm::SlidingWindowLog {
max_requests: 100,
window: Duration::from_secs(60),
};
API Reference
Core Types
RateLimiter- Main rate limiter structRateLimiterBuilder- Builder for configuring rate limitersAlgorithm- Rate limiting algorithm enumRateLimitCheckResult- Result of a rate limit check
Stores
MemoryStore- In-memory storage using DashMapRedisStore- Redis-backed distributed storage (requiresredisfeature)
Middleware
RateLimitMiddleware- HTTP middleware for rate limitingKeyExtractor- Strategies for extracting rate limit keysRequestInfo- Request information for key extraction
Errors
RateLimitError- Error types for rate limiting operationsRateLimitHeaders- Standard rate limit response headers
Summary
Rate limiting is essential for production APIs. Key takeaways:
- Choose the right algorithm for your use case
- Use Redis for multi-instance deployments
- Include headers so clients can self-regulate
- Fail open to maintain availability
- Use tiered limits for different user classes
- Handle proxy IPs correctly