GlobalRateLimiter usage rules

Copy Markdown View Source

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