reliakit-ratelimit
A clock-agnostic token-bucket rate limiter for Rust.
A token bucket is a simple, well-understood way to cap how often something may
happen. The bucket holds up to capacity tokens and gains refill_amount
tokens every refill_interval; each request spends one or more tokens, and when
the bucket is empty requests are denied until it refills. Two numbers describe
the whole policy:
- capacity — the largest burst you will allow at once.
- refill rate (
refill_amountperrefill_interval) — the sustained rate once the burst is spent.
reliakit-ratelimit implements this with no dependencies, no std, and no
hidden behavior. It does not read the clock, sleep, spawn, or allocate — you
pass the current time in, and you decide what to do when a request is denied.
That makes it equally usable from synchronous code, any async runtime, and
no_std / embedded targets, and every decision is deterministic and trivial to
unit-test. All arithmetic is integer-only and saturating, so no call can
overflow or panic.
Why "clock-agnostic"?
Most rate limiters reach for std::time::Instant or a runtime timer. This one
takes the current time as a u64 argument in whatever monotonic unit you choose
(milliseconds is typical) and uses that same unit for refill_interval:
- Runtime-neutral — Tokio, async-std, blocking threads, or a bare-metal loop.
no_std-friendly — CI builds it forthumbv7em-none-eabi.- Deterministic & testable — behavior depends only on the timestamps you pass; tests assert exact token counts and wait times with no sleeping.
A clock that briefly moves backwards is handled with saturating arithmetic (no refill happens, nothing panics).
Installation
[]
= "0.1"
This crate is #![no_std] and has no feature flags; it depends only on core.
Usage
The bucket starts full, so an initial burst up to capacity is allowed:
use RateLimiter;
// Capacity 10, refill 1 token every 100ms — about 10 requests/second sustained,
// with bursts of up to 10.
let mut limiter = new;
let now = now_millis; // your own monotonic clock, in milliseconds
if limiter.try_acquire_one else
Take several tokens at once (e.g. a request that costs more), and tell a caller when to come back:
use RateLimiter;
let mut limiter = new;
let now = now_millis;
if !limiter.try_acquire
See examples/basic.rs for a complete loop that bursts,
gets throttled, and recovers.
API
| Method | Purpose |
|---|---|
RateLimiter::new(capacity, refill_amount, refill_interval) |
Construct a limiter (const fn). Starts full. |
try_acquire(now, tokens) -> bool |
Take tokens if available; consumes nothing on failure. |
try_acquire_one(now) -> bool |
Take a single token. |
available(now) -> u64 |
Tokens available now, after refilling. |
retry_after(now, tokens) -> Option<u64> |
Time until tokens are available (Some(0) if now; None if tokens > capacity). |
Choosing the numbers
The refill rate is refill_amount / refill_interval in your time unit. A few
examples, using milliseconds:
| Policy | new(capacity, refill_amount, refill_interval) |
|---|---|
| 10 req/sec, burst 10 | RateLimiter::new(10, 1, 100) |
| 100 req/sec, burst 20 | RateLimiter::new(20, 1, 10) |
| 5 req/sec, burst 5 | RateLimiter::new(5, 1, 200) |
| 600 req/min, burst 60 | RateLimiter::new(60, 10, 1_000) |
capacity and the refill rate are independent: a large capacity with a slow
refill allows a big one-off burst but a low steady rate.
Concurrency
RateLimiter is a plain value and is not internally synchronized (no atomics
— keeping it dependency-free and no_std). To share one limiter across threads
or tasks, wrap it in your own Mutex/lock. For per-key limiting, keep a separate
limiter per key.
Pairs well with the rest of Reliakit
Use it alongside reliakit-circuit
(stop calling a dependency that is down) and
reliakit-backoff (space out
retries). All three are clock-agnostic and no_std.
When to use it
- Capping calls to an API, a database, or any shared resource.
- Smoothing bursty work into a steady rate.
- Embedded or runtime-agnostic code that still needs throttling.
When not to use it
- Distributed rate limiting across many processes — this is an in-process limiter; coordinate through a shared store for a global limit.
- Precise sub-token (fractional) rates — the bucket works in whole tokens; scale your unit (e.g. count in tenths) if you need finer granularity.
Safety
This crate is #![forbid(unsafe_code)] and #![no_std]. All arithmetic
saturates; no method panics on any input, including a non-monotonic clock.
Minimum Supported Rust Version
Rust 1.85 and newer. No nightly features are used.
License
Licensed under the MIT License. See LICENSE.