Skip to main content

FixedWindowRateLimiter

Struct FixedWindowRateLimiter 

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

  1. AT MOST max_tracked_clients ENTRIES PER MAP, so at most 2 * max_tracked_clients entries 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.
  2. AT MOST MAX_TRACKED_CLIENT_ID_LEN BYTES 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 (a HashMap keeps its load under 7/8, so 4096 entries take the next power of two up) of 32 bytes each — a 16-byte Box<str> handle and a ClientBudget’s two u64s — which is 256 KiB. About 768 KiB a map, so about 1.5 MiB for both at the defaults.
  3. 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

Source

pub fn new() -> Self

A limiter with the reasoned defaults documented at the module level.

Source

pub fn with_config(config: RateLimitConfig) -> Self

A limiter with a host’s own budgets.

Source

pub fn config(&self) -> &RateLimitConfig

The configuration in force.

Source

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.

Source

pub fn tracked_authorization_clients(&self) -> usize

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

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for FixedWindowRateLimiter

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl RateLimiter for FixedWindowRateLimiter

Source§

fn check(&self, attempt: Attempt<'_>) -> RateLimitDecision

Decide whether attempt may proceed. Called BEFORE any credential is evaluated, so a Deny costs the attacker a lookup and tells them nothing.
Source§

fn record(&self, attempt: Attempt<'_>, outcome: AttemptOutcome)

Report how an allowed attempt turned out. Defaults to doing nothing, so a host that only wants a hard ceiling implements one method.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.