Skip to main content

Module rate_limit

Module rate_limit 

Source
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:

  1. endpoint: endpoint_rate_limits[path] (the config-level per-endpoint map), keyed by (ip, path);
  2. route: the route decorator’s rate_limit (window default 60, rate_limit_window or 60), keyed by (ip, path);
  3. geo: the route decorator’s geo_rate_limits country map, the resolved country’s entry with the "*" fallback (country in limits else "*" in limits), keyed by (ip, path);
  4. global: the config rate_limit/rate_limit_window, keyed by ip alone.

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§

RateLimitConfig
The rate-limiting knobs (the reference enable_rate_limiting / rate_limit / rate_limit_window / enable_rate_limit_auto_ban group).
RateLimitConfigError
An invalid RateLimitConfig or RouteRateLimits: the config error RateLimiter::new fails closed with.
RateLimitDecision
The outcome of one rate-limit check.
RateLimitEntry
One (requests, window) rate-limit entry: a per-endpoint or per-country tier override (the reference endpoint_rate_limits[path] / geo_rate_limits[country] (int, int) tuples).
RateLimiter
The sliding-window rate limiter over one shared in-memory store.
RouteRateLimits
The route decorator’s rate-limit tier overrides (RouteConfig.rate_limit / rate_limit_window / geo_rate_limits).
TierDecision
The outcome of one tiered rate-limit check (RateLimiter::check_tiers).

Enums§

RateLimitTier
The rate-limit tier a RateLimiter::check_tiers decision 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_limit decorator default).
MAX_TRACKED_RATE_LIMIT_KEYS
The window store’s key cap (_MAX_TRACKED_RATE_LIMIT_KEYS / maxTrackedRateLimitKeys in 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.