Token Bucket Rate Limiter in Rust

Refill a token bucket from a monotonic clock, allow short bursts, and compute how long a caller must wait when it is empty.

RustResilience

Code

snippet
use std::time::{Duration, Instant};

struct TokenBucket {
    capacity: f64,
    tokens: f64,
    refill_per_second: f64,
    last_refill: Instant,
}

impl TokenBucket {
    fn new(capacity: f64, refill_per_second: f64) -> Result<Self, String> {
        if capacity <= 0.0 || refill_per_second <= 0.0 {
            return Err("capacity and refill rate must be positive".to_string());
        }
        Ok(Self {
            capacity,
            tokens: capacity,
            refill_per_second,
            last_refill: Instant::now(),
        })
    }

    fn refill(&mut self) {
        let now = Instant::now();
        let elapsed = now.duration_since(self.last_refill).as_secs_f64();
        self.tokens = (self.tokens + elapsed * self.refill_per_second).min(self.capacity);
        self.last_refill = now;
    }

    fn try_acquire(&mut self, cost: f64) -> Result<(), Duration> {
        assert!(
            cost > 0.0 && cost <= self.capacity,
            "cost must be within the bucket capacity"
        );
        self.refill();
        if self.tokens >= cost {
            self.tokens -= cost;
            return Ok(());
        }
        let missing = cost - self.tokens;
        Err(Duration::from_secs_f64(missing / self.refill_per_second))
    }
}

fn main() {
    let mut bucket = TokenBucket::new(2.0, 4.0).expect("valid configuration");
    for request in 1..=3 {
        match bucket.try_acquire(1.0) {
            Ok(()) => println!("request {request}: allowed"),
            Err(wait) => println!("request {request}: denied, retry in {wait:?}"),
        }
    }
    std::thread::sleep(Duration::from_millis(260));
    match bucket.try_acquire(1.0) {
        Ok(()) => println!("after refill: allowed"),
        Err(wait) => println!("after refill: denied, retry in {wait:?}"),
    }
}