Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
234: Building a Simple Rate Limiter Middleware in Axum
When you first start thinking about rate limiting, it's tempting to think of it as a simple counting problem. You'll probably tell yourself, "I'll just keep a HashMap of IP addresses and a counter in my app state. Every time a request comes in, I'll increment the number. If it's over 100, I'll return a 429."
The "Simple HashMap" Trap
The problem is that Axum is asynchronous and multi-threaded. If you try to wrap a HashMap in a Mutex and update it inside a handler or middleware, you aren't just solving a counting problem; you're creating a massive synchronization bottleneck.
// DON'T DO THIS in a high-traffic app
async fn rate_limit_middleware(
State(state): State<Arc<Mutex<HashMap<IpAddr, u32>>>>,
req: Request,
next: Next,
) -> Response {
let mut counts = state.lock().unwrap(); // Every single request blocks here
let ip = req.extensions().get<ConnectInfo<SocketAddr>>().map(|i| i.0.ip());
// ... increment and check ...
next.run(req).await
}
In the example above, every single request to your server—regardless of whether it's from the same user or a completely different one—must wait in line for that one Mutex lock. You've effectively turned your high-performance asynchronous server into a single-threaded one. Under a real DDoS attack (the exact time you actually need a rate limiter), your server will spend more time fighting over the lock than actually processing requests.
Using Atomic State and Fixed Windows
To do this right, we need to minimize the time we hold locks or avoid them entirely for the "hot path." For a simple in-memory limiter, I recommend starting with a "Fixed Window" approach using a thread-safe store. Instead of one giant lock, we can use a DashMap (a concurrent hash map) or, for even better performance, a dedicated crate like governor. But since we're learning how this works under the hood, let's implement a basic version using std::sync::atomic for the counters.
We'll create a state object that tracks the current "window" (e.g., the current minute) and the request count for each IP. If the window changes, we reset the count.
use axum::{
extract::{ConnectInfo, State},
http::{Request, StatusCode},
middleware::Next,
response::Response,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::net::IpAddr;
use std::time::{SystemTime, UNIX_EPOCH};
struct RateLimitState {
// We use a Mutex here for the Map, but the values inside could be Atomics
// For this simple example, we'll keep it straightforward.
requests: Mutex<HashMap<(IpAddr, u64), u32>>,
}
async fn rate_limiter(
State(state): State<Arc<RateLimitState>>,
ConnectInfo(addr): ConnectInfo<std::net::SocketAddr>,
req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() / 60; // Window = 1 minute
let ip = addr.ip();
{
let mut map = state.requests.lock().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let count = map.entry((ip, now)).or_insert(0);
*count += 1;
if *count > 100 {
return Err(StatusCode::TOO_MANY_REQUESTS);
}
} // Lock is dropped here immediately
Ok(next.run(req).await)
}
Integrating the Middleware into the Router
The beauty of middleware::from_fn is that it allows us to treat our rate limiter as a wrapper. I usually apply this globally, but you can also apply it to specific "expensive" routes (like /api/search or /api/login) to protect them without affecting static assets.
Keep in mind: the ConnectInfo extractor requires you to use a specific transport layer in your server setup (like Router::into_make_service_with_connect_info), otherwise the middleware will panic because it can't find the IP address of the requester. I've seen plenty of developers spend an hour wondering why their middleware is crashing, only to realize they forgot to enable the connection info at the listener level.
let state = Arc::new(RateLimitState {
requests: Mutex::new(HashMap::new()),
});
let app = Router::new()
.route("/", get(|| async { "Hello!" }))
.layer(axum::middleware::from_fn_with_state(state.clone(), rate_limiter));
// IMPORTANT: You must use into_make_service_with_connect_info
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(
listener,
app.into_make_service_with_connect_info<std::net::SocketAddr>()
).await.unwrap();📋 Practical Task
Exercise: Implementing a Tiered Rate Limiter for API Keys
In a real-world scenario, you rarely rate limit by IP alone. Usually, you have different tiers (e.g., "Free" users get 10 requests/min, "Premium" users get 1000 requests/min).
Your Task: Modify the rate limiter middleware implemented in the lesson to support API Key-based limiting instead of IP-based limiting.
- Create a
UserTierenum withFreeandPremiumvariants. - Create a mock
UserDatabase(a simpleHashMap<String, UserTier>) that maps API keys to their tier. - Update the middleware to extract an
x-api-keyheader from the request. - If the key is missing, return
401 Unauthorized. - If the key exists, look up their tier and apply the corresponding limit (10 for Free, 1000 for Premium).
- Ensure the lock on the request map is held for the shortest time possible.
There are no comments for now.