Expand description
Rate limiting for the Churust web framework.
RateLimit admits a bounded number of requests per key per period and
answers everything above that with 429 Too Many Requests and a
Retry-After header. It is both a Plugin (server-wide) and a
Middleware (scoped to part of the route tree with
RouteBuilder::intercept).
use churust_core::{Call, Churust, TestClient};
use churust_ratelimit::RateLimit;
let app = Churust::server()
.install(RateLimit::per_minute(2))
.routing(|r| {
r.get("/", |_c: Call| async { "ok" });
})
.build();
let client = TestClient::new(app);
assert_eq!(client.get("/").send().await.status().as_u16(), 200);
assert_eq!(client.get("/").send().await.status().as_u16(), 200);
let limited = client.get("/").send().await;
assert_eq!(limited.status().as_u16(), 429);
assert!(limited.header("retry-after").is_some());§The algorithm
A generic cell rate algorithm (GCRA), which is a leaky bucket expressed as one timestamp per key rather than a counter plus a timer. Each key stores a theoretical arrival time: the earliest moment at which the next request would be perfectly conforming. A request is admitted when it arrives no more than the burst tolerance ahead of that time, and the timestamp then advances by one emission interval.
Two properties follow, and both are why this is preferred over a fixed
window counter. Requests are smoothed rather than admitted in a stampede at
the top of each window, and Retry-After is an exact figure that falls out
of the arithmetic instead of an estimate.
§Choosing a key
The default key is the connection’s peer address, without the port, so several connections from one client share a bucket. An IPv4 peer is keyed on the whole address; an IPv6 peer is keyed on its /64 prefix, because the low 64 bits of an IPv6 address are the interface identifier and the host picks those itself. Without the mask, a peer that walks its own subnet gets a fresh allowance per address and is never limited at all, and even an honest client rotating privacy addresses drifts out of its bucket. The cost of the mask is that hosts which genuinely share one /64 share a budget; if that is wrong for your deployment, key on the full address yourself:
use churust_ratelimit::RateLimit;
let limiter = RateLimit::per_minute(60)
.by(|call| call.peer_addr().map(|addr| addr.ip().to_string()));Behind a reverse proxy the peer address is the proxy, which would put every
visitor in one bucket. Use RateLimit::by there too, and read a
forwarding header only after checking
Call::peer_addr against proxies you
actually trust. X-Forwarded-For is caller-supplied and trivially spoofed.
Structs§
- Rate
Limit - A rate limiter, usable as a plugin or as scoped middleware.