pub struct FixedWindowRateLimiter { /* private fields */ }Expand description
An in-memory, per-process, weighted fixed-window RateLimiter the crate ships so that
throttling is one line rather than a project.
Read the module documentation before installing it. In particular: it is PER PROCESS, so on a multi-node deployment the effective limit is multiplied by the node count.
§How it is bounded
A limiter that grows a map keyed on an attacker-supplied client_id is itself a denial of
service, so the maps are bounded three ways at once and every bound is a hard one.
THERE ARE TWO MAPS, which is the first thing to hold on to, because every figure below is a
figure per map: one keyed on client_id for Attempt::ClientAuthentication and one for
Attempt::AuthorizationRequest, kept apart so that neither endpoint can spend the other’s
budget (see Window::authorization_counter). Each is capped INDEPENDENTLY, so every bound
below is doubled in total. The authorization map is the one an attacker reaches first, because
its caller has not authenticated: the identifier it is keyed on is whatever arrived in the
query string.
- AT MOST
max_tracked_clientsENTRIES PER MAP, so at most2 * max_tracked_clientsentries in all — 8192 at the defaults. When a map is full, an identifier that is not already in it is charged against that map’s single shared OVERFLOW counter instead of getting an entry of its own. Nothing is allocated for it. - AT MOST
MAX_TRACKED_CLIENT_ID_LENBYTES OF KEY. A longer identifier goes straight to the overflow counter, so the worst case is bounded in bytes and not only in entries. Per map: 4096 keys of 128 bytes on the heap is 512 KiB, and the table holding them is 8192 slots (aHashMapkeeps its load under 7/8, so 4096 entries take the next power of two up) of 32 bytes each — a 16-byteBox<str>handle and aClientBudget’s twou64s — which is 256 KiB. About 768 KiB a map, so about 1.5 MiB for both at the defaults. - AT MOST ONE WINDOW OF LIFETIME. BOTH maps are cleared when the window rolls, which costs nothing semantically because every counter in them was about to be reset anyway. No entry survives a window, so there is no eviction policy to get wrong and no slow leak of keys that were seen once.
FixedWindowRateLimiter::tracked_clients and
FixedWindowRateLimiter::tracked_authorization_clients report the two maps separately, so a
host — and this crate’s own gates — can SEE both bounds rather than watch one and infer the
other.
Each overflow counter FAILS CLOSED, which is the important half: a spray of a million distinct identifiers does not get a million fresh budgets, it gets one budget shared between all of them, so the spray throttles itself harder than a repeat offender would. The cost of that choice, and it is a real one, is that a legitimate client whose first authentication of a window arrives after an attacker has filled the map shares the overflow counter for the rest of that window. That is a bounded, self-clearing degradation, and it is preferable to the alternative (evicting live counters to make room) which would let an attacker RESET a budget on demand by spraying, turning the limiter off exactly when it is needed.
§Cost
One Mutex and one HashMap per limiter, allocated when the host constructs it and never
otherwise: a host that does not install this pays nothing, and crate::events::Hooks is
unchanged by its existence. Each check is one lock, one integer division and at most two hash
lookups — three when client_authentication_capacity_overrides is non-empty, and the extra one
is skipped entirely by an is_empty when it is not. The lock is held only for the arithmetic,
never across a store call or an await.
Implementations§
Source§impl FixedWindowRateLimiter
impl FixedWindowRateLimiter
Sourcepub fn with_config(config: RateLimitConfig) -> Self
pub fn with_config(config: RateLimitConfig) -> Self
A limiter with a host’s own budgets.
Sourcepub fn config(&self) -> &RateLimitConfig
pub fn config(&self) -> &RateLimitConfig
The configuration in force.
Sourcepub fn tracked_clients(&self) -> usize
pub fn tracked_clients(&self) -> usize
How many client_id values currently hold a CLIENT-AUTHENTICATION counter of their own.
Exposed so a host (and this crate’s own gate on the bound) can SEE that the map is bounded
rather than trust that it is. Never exceeds max_tracked_clients.
This is ONE of the two bounded maps, and it is not the one an attacker reaches first: see
FixedWindowRateLimiter::tracked_authorization_clients, which a gate watching only this
number is blind to.
How many client_id values currently hold an AUTHORIZATION-REQUEST counter of their own.
The sibling of FixedWindowRateLimiter::tracked_clients, and the one to watch if only one
is watched: this map is filled by callers who have not authenticated at all, because the
identifier an /authorize request is keyed on is whatever arrived in the query string,
whereas the client-authentication map is filled by callers who at least presented a
credential. It is capped by the same max_tracked_clients and never exceeds it.
Trait Implementations§
Source§impl Debug for FixedWindowRateLimiter
impl Debug for FixedWindowRateLimiter
Source§impl Default for FixedWindowRateLimiter
impl Default for FixedWindowRateLimiter
Source§impl RateLimiter for FixedWindowRateLimiter
impl RateLimiter for FixedWindowRateLimiter
Source§fn check(&self, attempt: Attempt<'_>) -> RateLimitDecision
fn check(&self, attempt: Attempt<'_>) -> RateLimitDecision
attempt may proceed. Called BEFORE any credential is evaluated, so a
Deny costs the attacker a lookup and tells them nothing.