pub struct KeyedRateLimiter<C: Clock = SystemClock> { /* private fields */ }Expand description
Per-key sliding-window rate limiter backed by a DashMap.
Each unique key (IP address, user ID, etc.) gets its own independent counter.
The check-and-update sequence for a given key is atomic: no TOCTOU race can
allow more requests than max_requests in any single window, even under
high concurrency. Distinct keys live on different shards and never block
each other on the update path.
The map is capped at DEFAULT_MAX_ENTRIES keys: when an insert would push
len() past the cap the entry with the oldest window_start is evicted
first. The cap is enforced strictly — the check, eviction, and insert
for new keys all run inside a single insert_guard critical section, so
len() never exceeds max_entries at any observable instant. Updates to
already-present keys take the lock-free fast path and never contend on
insert_guard.
§Deployment note
This rate limiter is per-process. In a multi-replica deployment, each
replica enforces the limit independently — the effective limit across N
replicas is N × limit. For true distributed enforcement, configure a
Redis-backed rate limiter via the redis-rate-limiting Cargo feature (see
the fraiseql-observers queue feature for the integration pattern). Call
warn_if_single_node_rate_limiting during server startup to emit a
reminder when no distributed backend is detected.
§Type parameter
C: Clock selects the time source. Production code uses the default
SystemClock (a zero-sized type) so the clock is inlined and no virtual
dispatch or heap allocation occurs. Tests can substitute any closure or
custom clock via KeyedRateLimiter::with_clock.
§Constructors
KeyedRateLimiter::new— use the system wall clock (production).KeyedRateLimiter::with_clock— inject a custom clock (testing).KeyedRateLimiter::with_clock_and_max_entries— custom clock + cap (testing).
Implementations§
Source§impl KeyedRateLimiter<SystemClock>
impl KeyedRateLimiter<SystemClock>
Sourcepub fn new(config: AuthRateLimitConfig) -> Self
pub fn new(config: AuthRateLimitConfig) -> Self
Create a new keyed rate limiter using wall-clock time.
Sourcepub fn with_max_entries(config: AuthRateLimitConfig, max_entries: usize) -> Self
pub fn with_max_entries(config: AuthRateLimitConfig, max_entries: usize) -> Self
Create a rate limiter with a custom entry cap.
Use this when the deployment context calls for a tighter or looser bound
than DEFAULT_MAX_ENTRIES. Setting max_entries = 0 disables the cap
(unbounded — not recommended in production).
Source§impl<C: Clock> KeyedRateLimiter<C>
impl<C: Clock> KeyedRateLimiter<C>
Sourcepub fn with_clock(config: AuthRateLimitConfig, clock: C) -> Self
pub fn with_clock(config: AuthRateLimitConfig, clock: C) -> Self
Create a rate limiter with an injectable clock (for testing).
The clock’s now_unix_secs method is called on every check() to
obtain the current Unix timestamp. Pass || u64::MAX to simulate a
broken system clock and verify fail-open behavior.
Sourcepub fn with_clock_and_max_entries(
config: AuthRateLimitConfig,
max_entries: usize,
clock: C,
) -> Self
pub fn with_clock_and_max_entries( config: AuthRateLimitConfig, max_entries: usize, clock: C, ) -> Self
Create a rate limiter with both a custom clock and a custom entry cap (for testing).
Combines the benefits of KeyedRateLimiter::with_clock and
KeyedRateLimiter::with_max_entries for deterministic eviction tests.
Sourcepub fn check(&self, key: &str) -> Result<()>
pub fn check(&self, key: &str) -> Result<()>
Check if a request should be allowed for the given key
§Atomicity
The check-and-update step for a given key is atomic: while
inspecting and mutating the RequestRecord for key, this function
holds the per-shard write reference for that key. No concurrent thread
can observe a partial state for the same key, which prevents the
classic TOCTOU race where multiple threads simultaneously exceed the
rate limit.
§Capacity cap
When max_entries > 0 the map’s length is enforced strictly:
new-key inserts run under a serialising insert_guard, so the
cap-check, oldest-entry eviction, and insert all occur in a single
critical section. records.len() <= max_entries therefore holds at
every observable instant, including under sustained concurrent burst.
The fast path (updates to keys already present) does not acquire the
guard and runs lock-free.
The periodic expiry sweep is best-effort and runs outside the guard;
it only ever shrinks records, so it cannot push len() over the cap.
§Returns
Ok(()) if the request is allowed and the counter has been incremented.
§Errors
Returns AuthError::RateLimited if the key has exceeded the configured
rate limit within the sliding window.
Sourcepub fn active_limiters(&self) -> usize
pub fn active_limiters(&self) -> usize
Get the number of active rate limiters (for monitoring).
Returns the authoritative entry count maintained under insert_guard,
not DashMap’s per-shard sum. Reads are lock-free and reflect the
post-mutation count at every observable instant.
Sourcepub fn clone_config(&self) -> AuthRateLimitConfig
pub fn clone_config(&self) -> AuthRateLimitConfig
Create a copy for independent testing
Trait Implementations§
Auto Trait Implementations§
impl<C = SystemClock> !Freeze for KeyedRateLimiter<C>
impl<C = SystemClock> !RefUnwindSafe for KeyedRateLimiter<C>
impl<C = SystemClock> !UnwindSafe for KeyedRateLimiter<C>
impl<C> Send for KeyedRateLimiter<C>
impl<C> Sync for KeyedRateLimiter<C>
impl<C> Unpin for KeyedRateLimiter<C>where
C: Unpin,
impl<C> UnsafeUnpin for KeyedRateLimiter<C>where
C: UnsafeUnpin,
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more