rust 118 lines · 3 tabs

Token-Bucket Rate Limiter Middleware for Axum Using DashMap

Shared by codesnips Jul 2026
3 tabs
use dashmap::DashMap;
use std::time::Instant;

pub struct TokenBucket {
    tokens: f64,
    last_refill: Instant,
}

impl TokenBucket {
    fn new(capacity: f64) -> Self {
        TokenBucket { tokens: capacity, last_refill: Instant::now() }
    }

    fn try_consume(&mut self, capacity: f64, refill_per_sec: f64) -> bool {
        let now = Instant::now();
        let elapsed = now.duration_since(self.last_refill).as_secs_f64();
        self.tokens = (self.tokens + elapsed * refill_per_sec).min(capacity);
        self.last_refill = now;

        if self.tokens >= 1.0 {
            self.tokens -= 1.0;
            true
        } else {
            false
        }
    }
}

#[derive(Clone)]
pub struct RateLimiter {
    buckets: std::sync::Arc<DashMap<String, TokenBucket>>,
    capacity: f64,
    refill_per_sec: f64,
}

impl RateLimiter {
    pub fn new(capacity: f64, refill_per_sec: f64) -> Self {
        RateLimiter {
            buckets: std::sync::Arc::new(DashMap::new()),
            capacity,
            refill_per_sec,
        }
    }

    pub fn check(&self, key: &str) -> bool {
        let mut entry = self
            .buckets
            .entry(key.to_owned())
            .or_insert_with(|| TokenBucket::new(self.capacity));
        entry.try_consume(self.capacity, self.refill_per_sec)
    }
}
3 files · rust Explain with highlit

This snippet shows a per-client rate limiter for an axum service built on the token-bucket algorithm. The bucket refills continuously at a fixed rate and each request consumes one token; when the bucket is empty the request is rejected with 429 Too Many Requests. This model is preferable to a fixed-window counter because it smooths bursts naturally and never suffers the boundary spike where two windows meet.

In token_bucket.rs, TokenBucket stores tokens as an f64 plus the last_refill instant. try_consume computes elapsed time lazily on each call, adds elapsed * refill_per_sec tokens capped at capacity, and only then checks whether one token is available. Computing refill lazily avoids a background timer task per client — the state is cheap and only advances when the client is actually seen. RateLimiter wraps a DashMap<String, TokenBucket> so buckets for different keys can be read and mutated concurrently without a global lock; DashMap shards internally, and entry().or_insert_with() makes first-touch creation atomic per key.

In middleware.rs, rate_limit is an axum::middleware::from_fn_with_state handler. It derives a client key from the connection's SocketAddr (a real deployment behind a proxy would prefer a trusted X-Forwarded-For or an API key), calls limiter.check(&key), and either forwards the request via next.run(req).await or short-circuits with a 429. The rejection response sets a Retry-After header so well-behaved clients back off. Returning Result<Response, StatusCode> keeps the handler ergonomic while still producing a proper HTTP response.

In main.rs, the limiter is constructed once, wrapped in shared state, and attached with .layer(...) so every route inherits the limit. Because the middleware runs before the handler, rejected requests never touch business logic. into_make_service_with_connect_info is required so the peer SocketAddr is available for keying.

The main trade-off is that this state is per-process: horizontal scaling needs a shared store like Redis instead of DashMap. It also grows memory with distinct keys, so a production version would evict idle buckets. For a single instance, however, this approach is fast, lock-light, and simple to reason about.


Related snips

Share this code

Here's the card — post it anywhere.

Token-Bucket Rate Limiter Middleware for Axum Using DashMap — share card
Link copied