Rate limiting by a generic cell rate algorithm (GCRA), in ETS.
Each key has one theoretical arrival time (TAT), advanced by one emission
interval per request with compare-and-swap, so concurrent requests on one
key can never both take the last slot. A limiter allows count requests
per period on average, and bursts of up to burst.
Start the table once, in the host's supervision tree:
children = [Macaw.Plug.RateLimit.Table, ...]Then limit with the plug, keyed by the client's address (after
Macaw.Plug.ClientIp) unless told otherwise:
plug Macaw.Plug.RateLimit, id: :api, rate: {50, :second}, burst: 200or call check/3 directly. Over the limit, the plug answers 429 with a
Retry-After header and halts; :on_limit, a {module, function} taking
the conn and the seconds to wait, answers instead (a JSON API's error
shape, say).
Limiters share one table, each key stored as {id, key}. Keys idle long
enough to have a full burst again are the same as absent, and the table's
sweep removes them, so memory follows the active keys, not every key ever
seen.
Summary
Functions
Takes a slot for key at now (monotonic microseconds, default the
clock): :ok, or {:error, seconds_to_wait}, at least 1.
A limiter: id names it, rate: {count, :second | :minute | :hour} is its
average, and burst (default count) the most requests it takes at once.
Raises on a rate it can't honor, naming it.
Types
@type t() :: %Macaw.Plug.RateLimit{ id: term(), interval: pos_integer(), tolerance: non_neg_integer() }
Functions
@spec check(t(), term(), integer()) :: :ok | {:error, pos_integer()}
Takes a slot for key at now (monotonic microseconds, default the
clock): :ok, or {:error, seconds_to_wait}, at least 1.
A limiter: id names it, rate: {count, :second | :minute | :hour} is its
average, and burst (default count) the most requests it takes at once.
Raises on a rate it can't honor, naming it.