Per-node fixed-window rate limiter backed by a lazily created ETS table.
Each key owns one counter per window: the first hit opens a window of
window_ms and every hit until it closes increments the counter, so a
key may make at most limit hits per window. Once the window closes the
next hit opens a fresh one. Fixed windows admit up to 2 × limit hits
across a window boundary; pick limit/window_ms with that in mind.
The table is created on first use and needs no process in your
supervision tree. Stale windows are swept opportunistically (every
1000 hits) and can be swept explicitly with sweep/1.
Per-node only
Counters live on the local node. In a clustered deployment each node
limits independently, so a wallet load-balanced across n nodes gets
up to n × limit hits per window. Use a shared-store implementation of
X402.RateLimiter when that matters.
Summary
Functions
Records a hit for key in the fixed window of window_ms milliseconds.
Clears every counter in the table.
Removes windows that have already closed.
Types
@type table() :: atom()
ETS table name holding the counters.
Functions
@spec hit(table() | nil, X402.RateLimiter.key(), pos_integer(), pos_integer()) :: X402.RateLimiter.result()
Records a hit for key in the fixed window of window_ms milliseconds.
table is the ETS table name; nil selects the default table.
Examples
iex> table = String.to_atom("rate_limiter_doctest_122916")
iex> X402.RateLimiter.ETS.hit(table, {:payer, "0xabc"}, 2, 60_000)
{:allow, 1}
iex> X402.RateLimiter.ETS.hit(table, {:payer, "0xabc"}, 2, 60_000)
{:allow, 0}
iex> {:deny, retry_after_ms} = X402.RateLimiter.ETS.hit(table, {:payer, "0xabc"}, 2, 60_000)
iex> retry_after_ms in 1..60_000
true
@spec reset(table() | nil) :: :ok
Clears every counter in the table.
@spec sweep(table() | nil) :: non_neg_integer()
Removes windows that have already closed.
Returns the number of rows removed.