livery_ratelimit (livery v0.2.0)

View Source

Per-key rate-limiting / throttling middleware (token bucket).

Each client (by default identified by its Authorization bearer token) gets a token bucket: Capacity tokens that refill at RefillPerSec. A request consumes one token; when the bucket is empty the request is shed with 429 Too Many Requests. A request with no key passes through unlimited.

Build the stack entry with limiter/2,3 (which allocates an isolated keyspace):

Stack = [
    {livery_ratelimit, livery_ratelimit:limiter(100, 10)}  %% burst 100, 10/s
].

"N requests per minute" maps to limiter(N, N/60). Per-key state lives in the supervised livery_ratelimit_store ETS table; the raw key is never stored (it is SHA-256 hashed). Responses carry RateLimit-Limit/-Remaining/-Reset (and Retry-After on a 429) unless headers => false.

Security: the default key is the bearer token, which the client controls. That gives per-credential quotas, NOT protection against an unauthenticated flood: a client rotating tokens gets a fresh bucket each time. Client IP is not yet surfaced by the wire libs, so for flood protection key on a trusted identity via a custom key fun (an authenticated user id, or a forwarded-IP header you trust because you terminate behind a known proxy). The store caps its key count (see livery_ratelimit_store) so distinct-key floods bound memory regardless.

Summary

Functions

Throttle the request by its key, or pass through when keyless.

Build a limiter: Capacity burst tokens refilling at RefillPerSec.

limiter/2 with options: name, key (a req -> binary | undefined fun, default the bearer token), status (default 429), body, and headers (default true).

Types

key_fun()

-type key_fun() :: fun((livery_req:req()) -> binary() | undefined).

state()

-type state() ::
          #{name := term(),
            capacity := non_neg_integer(),
            rate := number(),
            key := key_fun(),
            status := 100..599,
            body := iodata(),
            headers := boolean()}.

Functions

call/3

Throttle the request by its key, or pass through when keyless.

limiter(Capacity, RefillPerSec)

-spec limiter(non_neg_integer(), number()) -> state().

Build a limiter: Capacity burst tokens refilling at RefillPerSec.

limiter(Capacity, RefillPerSec, Opts)

-spec limiter(non_neg_integer(), number(), map()) -> state().

limiter/2 with options: name, key (a req -> binary | undefined fun, default the bearer token), status (default 429), body, and headers (default true).