ratelimit
Rate limiting for Rust — GCRA (Generic Cell Rate Algorithm) with pluggable backends (in-memory, Redis) and optional Tower layer integration.
Feature Flags
| Feature | Default | Description |
|---|---|---|
in-memory |
✅ | InMemoryBackend — GCRA state in a DashMap, one u64 TAC per key. |
sliding-window |
— | SlidingWindowBackend — alternative sliding-window counting backend. |
redis |
— | RedisBackend for distributed, multi-instance rate limiting. |
sqlite |
— | SqliteBackend for durable local limits. |
tower |
— | RateLimitLayer with X-RateLimit-* headers and client_ip identity resolution. |
metrics |
— | Record per-check results via the metrics facade. |
test-util |
— | NoopBackend — always-allow backend for tests and kill switches. |
Features
- GCRA algorithm — smooth, memory-efficient rate limiting
- In-memory backend via
DashMap(default) - Optional Redis backend for distributed deployments
- Tower
LayerwithX-RateLimit-*response headers - Configurable burst / token-bucket capacity
What is GCRA?
The Generic Cell Rate Algorithm treats each key as a leaky bucket. A request is allowed if the bucket has capacity; otherwise it is rejected. Tokens are replenished at a constant rate derived from the desired RPS/RPM.
allowed if: ema ≤ limit
where ema(t) = max(0, ema(t-1) - (t - t_last)) + 1
This yields the same behaviour as a token bucket but only stores the last
emission time — one Instant per key.
Quick Start
use ;
async
Presets
| Constructor | Rate | Burst |
|---|---|---|
Quota::per_second |
n / s | n |
Quota::per_minute |
n / 60 s | n |
Quota::per_hour |
n / 3600 s | n |
Override burst with .allow_burst(n).
Tower Integration
By default the layer keys requests by the client's socket address and
ignores X-Forwarded-For entirely (secure by default — a client-set XFF
header cannot mint fresh budgets).
use ;
use RateLimitLayer;
use ;
use SocketAddr;
let backend = new;
let layer = new;
let app = new
.route
.layer;
// Serve with connect info so the layer can see the peer address.
// Without it, requests are rejected with 503 (fail closed) unless a
// MissingClientPolicy::FallbackKey is configured.
let listener = bind.await?;
serve.await?;
Behind a proxy
If the service sits behind proxies you control, tell the layer which
peers it may believe, and how many header entries your infrastructure
appends in front of the direct peer (num_trusted_hops):
use ;
// internet → ALB (trusted) → nginx (trusted, direct peer) → app
// nginx appends the ALB's address, so XFF is "client, alb-ip";
// the default num_trusted_hops = 1 skips "alb-ip" and yields "client".
let layer = new
.with_client_ip;
Resolution walks the header RIGHT-TO-LEFT and never trusts entries left
of the resolved one, so client-spoofed XFF values are ignored. For a
single internet → nginx → app proxy, set num_trusted_hops: 0.
Platforms with a dedicated header (CF-Connecting-IP) can set
trusted_header; same trust rules apply.
Migrating from 0.3.0: if you were behind a proxy, you must configure
trusted_proxies (the old trust-XFF-unconditionally behavior is gone);
if your service is directly exposed, you need nothing — the default is
stricter and safe. Callers keying by API keys instead of IP can use
.with_key_extractor(...).
Comparison with governor
| ratelimit | governor | |
|---|---|---|
| Algorithm | GCRA (leaky bucket) | GCRA (leaky bucket) |
| Backends | In-memory, Redis | In-memory only |
| Tower layer | Optional | Built-in |
| Dependencies | Minimal | Jitter + parking_lot |
| Burst support | .allow_burst() |
Fixed per-quota |
Live Redis tests
The Redis GCRA decision loop (tests/redis_gcra_live.rs) needs a real
server, so it is fixture-gated like the rest of the estate:
CI runs the same suite against a redis:7 service automatically.
License
Licensed under either of MIT or Apache-2.0 at your option.
Security
Threat model: THREAT-MODEL.md.
Performance
Measured hot-path SLOs and allocation profile: PERF-SLO.md. Benchmarks run in CI (non-gating regression visibility against the saved ci baseline); the zero-allocation warm path is enforced by a counting-allocator test on every cargo test.
| Hot path (in-memory GCRA, warm key) | 1.0.0 | 1.1.0 |
|---|---|---|
| Heap allocations per check | 2 (key String + async-trait future box) |
0 (counting-allocator verified) |
Instructions per check (perf stat, min of 5) |
~1010 | ~662 (−34.5%) |
| Clock reads per check | 2 | 1 |
SLOs (idle hardware): check warm key < 200 ns P50, fresh key < 250 ns
amortized, resolve_client_identity < 150 ns proxied / < 20 ns direct.
The 2026-09-11 re-measurement ran on a saturated shared box (see the
caveat in PERF-SLO.md); instruction counts and allocation behavior are
the authoritative before/after evidence.
Head-to-head numbers against governor (the leading GCRA crate), with an honest feature comparison: COMPARISON.md.