Expand description
The sliding-window rate limiter: per client IP, global scope or per endpoint.
This is the Rust family’s in-memory port of the reference engine’s rate
limiter (guard_core/handlers/ratelimit_handler.py, via the Go port’s
ratelimit.go). One call, RateLimiter::check, records one request
into the window for (client IP, scope) and decides it:
evict every recorded timestamp at or before (now - window)
count = requests still inside the window (before this one is recorded)
record now
allowed = count < rate_limit§Counting semantics
The reference keeps a sliding log of request timestamps per key. The
in-memory store compares the pre-recording count against the limit
(allowed = count < limit), the Redis store compares the post-recording
rank against it (allowed = count <= limit, one Lua/ZSET pipeline keyed
rate_limit:rate:{ip}[:{endpoint hash}]); the boundary is the same, this
port mirrors the in-memory formulation because Redis is out of scope here:
the store is process-local, exactly what the reference falls back to when
Redis is off, and the same workers x rate_limit caveat applies to
multi-process deployments. The count reported on a block includes the
current request (count + 1), matching what both references log.
Blocked callers retry after the configured window: the reference attaches
Retry-After: <window seconds> to its 429 Too many requests, and
RateLimitDecision::retry_after carries the same value.
§Scope
The global scope keys the window by client IP alone (every endpoint
shares one budget per IP, the reference’s default pipeline tier). The
endpoint scope keys it by (client IP, endpoint path) - an isolated
budget per endpoint, the reference’s endpoint_path-keyed tier. Keys are
structured (IP + path fields), so no separator collision exists by
construction; the reference hashes the endpoint segment for its joined
Redis keys (rate:{ip}:{_hash_identity_segment(path)}), a concern the
in-memory store does not have.
§Tiers
RateLimiter::check_tiers runs the reference pipeline’s tier order
(RateLimitCheck.check, rate_limit.py, via the Go port’s
tiersFor/runTier): every configured tier records one request into
its own window and the first tier that crosses its limit blocks. The
tiers, in order:
- endpoint:
endpoint_rate_limits[path](the config-level per-endpoint map), keyed by(ip, path); - route: the route decorator’s
rate_limit(window default 60,rate_limit_window or 60), keyed by(ip, path); - geo: the route decorator’s
geo_rate_limitscountry map, the resolved country’s entry with the"*"fallback (country in limits else "*" in limits), keyed by(ip, path); - global: the config
rate_limit/rate_limit_window, keyed byipalone.
With no tiers configured the sequence collapses to the global tier, the
exact pre-tier behavior. The decision names the tier that blocked so
adapters can log the reference’s reason strings, and carries that
tier’s window for Retry-After.
§Honesty
No Redis: the distributed mode (shared budgets across workers, script
reload handling) is a follow-up. The window store is an LRU capped at
10 000 keys (maxTrackedRateLimitKeys in the Go port,
_MAX_TRACKED_RATE_LIMIT_KEYS in the reference), so abusive key
cardinality cannot grow the store without bound.
§Example
use std::net::IpAddr;
use std::str::FromStr;
use guard_core_engine::rate_limit::{RateLimitConfig, RateLimiter};
let limiter = RateLimiter::new(RateLimitConfig {
enable_rate_limiting: true,
rate_limit: 2,
..RateLimitConfig::default()
})
.expect("valid config");
let ip = IpAddr::from_str("192.0.2.1").unwrap();
assert!(limiter.check(ip, None).allowed);
assert!(limiter.check(ip, None).allowed);
// The third request inside the window crosses the limit.
let blocked = limiter.check(ip, None);
assert!(!blocked.allowed);
assert_eq!(blocked.retry_after(), 60);A tiered check runs every configured tier and names the one that blocked:
use std::collections::HashMap;
use std::net::IpAddr;
use std::str::FromStr;
use guard_core_engine::rate_limit::{
RateLimitConfig, RateLimitEntry, RateLimiter, RouteRateLimits,
};
let mut endpoint_rate_limits = HashMap::new();
endpoint_rate_limits.insert("/login".to_owned(), RateLimitEntry::new(1, 60).unwrap());
let limiter = RateLimiter::new(RateLimitConfig {
enable_rate_limiting: true,
endpoint_rate_limits,
..RateLimitConfig::default()
})
.expect("valid config");
let ip = IpAddr::from_str("192.0.2.1").unwrap();
assert!(limiter.check_tiers(ip, Some("/login"), None, None).allowed());
let blocked = limiter.check_tiers(ip, Some("/login"), None, None);
assert!(!blocked.allowed());
assert_eq!(blocked.tier_name(), "endpoint");A disabled limiter is inert: check allows without recording.
Structs§
- Rate
Limit Config - The rate-limiting knobs (the reference
enable_rate_limiting/rate_limit/rate_limit_window/enable_rate_limit_auto_bangroup). - Rate
Limit Config Error - An invalid
RateLimitConfigorRouteRateLimits: the config errorRateLimiter::newfails closed with. - Rate
Limit Decision - The outcome of one rate-limit check.
- Rate
Limit Entry - One
(requests, window)rate-limit entry: a per-endpoint or per-country tier override (the referenceendpoint_rate_limits[path]/geo_rate_limits[country](int, int)tuples). - Rate
Limiter - The sliding-window rate limiter over one shared in-memory store.
- Route
Rate Limits - The route decorator’s rate-limit tier overrides
(
RouteConfig.rate_limit/rate_limit_window/geo_rate_limits). - Tier
Decision - The outcome of one tiered rate-limit check
(
RateLimiter::check_tiers).
Enums§
- Rate
Limit Tier - The rate-limit tier a
RateLimiter::check_tiersdecision was made under (endpoint,route,geo,global- the reference check order).
Constants§
- DEFAULT_
RATE_ LIMIT - The reference default for
rate_limit(maximum requests per window). - DEFAULT_
RATE_ LIMIT_ WINDOW - The reference default for
rate_limit_window(seconds). - DEFAULT_
ROUTE_ RATE_ LIMIT_ WINDOW - The route tier’s default window (
rate_limit_window or 60, the@rate_limitdecorator default). - MAX_
TRACKED_ RATE_ LIMIT_ KEYS - The window store’s key cap (
_MAX_TRACKED_RATE_LIMIT_KEYS/maxTrackedRateLimitKeysin the references).
Functions§
- system_
clock - The system wall clock, in seconds since the Unix epoch: the clock every production store runs on. Adapters and stage tests that build several stores over one clock share this constructor.
Type Aliases§
- Clock
- The monotonic-ish wall clock, in seconds since the Unix epoch. Injectable so expiry behavior (window sliding) is testable without sleeping.