Current section
Files
Jump to
Current section
Files
global_rate_limiter
usage-rules.md
usage-rules.md
# GlobalRateLimiter usage rules
## Defining limits
- Build limits with `GlobalRateLimiter.Limit.new/2`, which validates its
options. Do not construct the `%GlobalRateLimiter.Limit{}` struct literal
directly.
- `:window` is a positive number of milliseconds; `:limit` is a non-negative
maximum count.
- The resource may be any Erlang term. Include every dimension that needs an
independent limit, such as an action and user, tenant, API key, or IP
address.
- Keep resource terms deterministic across nodes. Do not include a PID,
reference, or node-local value in a resource intended to be cluster-wide.
- The whole limit — resource, window, and limit — identifies the tracked
consumption group. The same resource with a different window or limit is
tracked independently, and changing a limit's window or limit value starts a
fresh consumption history. Use stable values for each logical limiter.
```elixir
limit =
GlobalRateLimiter.Limit.new({:password_challenge, client_ip},
window: 60_000,
limit: 3
)
```
## Consuming
- Use `GlobalRateLimiter.try_consume/2` when the operation must either start
immediately or be rejected. It returns `{:ok, remaining}`,
`{:error, {:rate_limited, retry_after_ms}}` when current consumption leaves
insufficient capacity, or `{:error, :count_exceeds_limit}` when the
requested count can never fit.
- Use `GlobalRateLimiter.consume/3` when the caller may block until capacity
is available. Prefer a finite timeout in request-handling processes.
- A successful call has already consumed capacity when it returns
`{:ok, remaining}`. Do not call `try_consume/2` again after a successful
consume.
- Do not implement check-then-consume logic with `remaining/1`. It is an
observational snapshot; `try_consume/2` and `consume/3` perform the atomic
capacity check.
- Counts must be positive. A count greater than the limit always returns
`{:error, :count_exceeds_limit}`.
```elixir
case GlobalRateLimiter.try_consume(limit, 1) do
{:ok, _remaining} -> perform_operation()
{:error, {:rate_limited, retry_after_ms}} -> retry_after(retry_after_ms)
{:error, :count_exceeds_limit} -> reject_invalid_count()
end
```
```elixir
case GlobalRateLimiter.consume(limit, 1, 5_000) do
{:ok, _remaining} -> perform_operation()
{:error, :timeout} -> retry_later()
{:error, :count_exceeds_limit} -> reject_operation()
end
```
## Waiting behavior
- Waiting callers are monitored. If a caller exits while queued, its entry is
removed without consuming capacity.
- A timeout leaves no reservation behind. A timeout of `0` succeeds only when
capacity is immediately available. There is no default timeout; pass
`:infinity` explicitly to wait without a deadline.
- FIFO ordering applies only to waiters for the same limit on one node.
Immediate consumers and waiters on other nodes may overtake them.
- `retry_after_ms` is a hint from the current cluster snapshot, not a
reservation. Another caller may consume the capacity first.
## Distribution and operational limits
- GlobalRateLimiter starts with the application; do not manually start its
`:pg` scope, registry, supervisor, counters, or waiters.
- Cluster-wide enforcement requires connected Erlang nodes. Consumption is
coordinated with `:global` and discovered through `:pg`.
- State is in memory. Node or application restarts discard that node's
counters, and a network partition allows each partition to enforce its own
view of the limit.
- GlobalRateLimiter is intended for low-traffic keys. Do not use it as a
high-throughput or durable distributed quota system.