Bourse.RateLimiter (bourse v0.1.0)

Copy Markdown View Source

Per-credential weighted rate limiter for exchange API requests.

Tracks request costs per {exchange_id, credential_key, bucket_axis} using a sliding window. Costs are summed (not counted) to handle weighted endpoints correctly.

Usage

# Authenticated request (per-API-key tracking)
key = {"binance", api_key, "ip"}
case Bourse.RateLimiter.check_rate(key, %{requests: 1200, period: 60_000}, 4) do
  :ok -> make_request()
  {:delay, ms} -> Process.sleep(ms); make_request()
end

# Or use wait_for_capacity which blocks until ready:
:ok = Bourse.RateLimiter.wait_for_capacity(key, rate_limit, cost)
make_request()

Credential Keys

The key is a tuple {exchange_id, credential_key, bucket_axis} where:

  • exchange_id is the exchange string ID ("binance", "bybit", etc.)
  • credential_key is either:
    • The API key string (for authenticated requests) -- isolates per-user limits
    • :public atom (for public requests) -- shared pool for unauthenticated requests
  • bucket_axis is the spec/header bucket axis ("request", "ip", "uid", "order_weight", etc.)

Summary

Types

A single bucket capacity check: {key, rate_limit, cost}.

Rate limiter key: {exchange_id, api_key | :public, bucket_axis}.

Rate limit configuration with max weight and period in milliseconds

Functions

Checks if a request can be made within rate limits.

Checks multiple bucket capacities atomically.

Returns a child specification for starting the rate limiter under a supervisor.

Gets current total cost for a key within a time window.

Records a request for a key with specified cost.

Resets rate limit tracking for a key.

Clears all rate-limit tracking state.

Starts the rate limiter.

Blocks until rate limit capacity is available, then records the request.

Types

bucket_check()

@type bucket_check() :: {key(), rate_limit() | nil, number()}

A single bucket capacity check: {key, rate_limit, cost}.

key()

@type key() ::
  {String.t(), String.t() | :public}
  | {String.t(), String.t() | :public, String.t()}

Rate limiter key: {exchange_id, api_key | :public, bucket_axis}.

rate_limit()

@type rate_limit() :: %{requests: pos_integer(), period: pos_integer()}

Rate limit configuration with max weight and period in milliseconds

Functions

check_rate(key, rate_limit, cost \\ 1, name \\ __MODULE__)

@spec check_rate(key(), rate_limit() | nil, number(), GenServer.name()) ::
  :ok | {:delay, pos_integer()}

Checks if a request can be made within rate limits.

Returns :ok if within limits (and records the request), or {:delay, milliseconds} if the caller should wait before making the request.

Parameters

  • key -- {exchange_id, api_key} or {exchange_id, :public} tuple
  • rate_limit -- %{requests: max_weight, period: period_ms} or nil (no limiting)
  • cost -- Request weight/cost (default: 1)
  • name -- GenServer name (default: Bourse.RateLimiter)

check_rates(bucket_checks, name \\ __MODULE__)

@spec check_rates([bucket_check()], GenServer.name()) :: :ok | {:delay, pos_integer()}

Checks multiple bucket capacities atomically.

Returns :ok only when every bucket has capacity, recording every cost in the same GenServer transition. Returns {:delay, milliseconds} without recording any bucket when at least one bucket is over limit.

child_spec(init_arg)

@spec child_spec(keyword()) :: Supervisor.child_spec()

Returns a child specification for starting the rate limiter under a supervisor.

get_cost(key, period, name \\ __MODULE__)

@spec get_cost(key(), pos_integer(), GenServer.name()) :: number()

Gets current total cost for a key within a time window.

Useful for debugging and monitoring.

record_request(key, cost \\ 1, name \\ __MODULE__)

@spec record_request(key(), number(), GenServer.name()) :: :ok

Records a request for a key with specified cost.

Called automatically by check_rate/4 when it returns :ok. Exposed for manual tracking if needed.

reset(key, name \\ __MODULE__)

@spec reset(key(), GenServer.name()) :: :ok

Resets rate limit tracking for a key.

reset_all(name \\ __MODULE__)

@spec reset_all(GenServer.name()) :: :ok

Clears all rate-limit tracking state.

Used by offline fixture gates so co-running live probes cannot poison capacity for Req.Test stub traffic (Task 179).

start_link(opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

Starts the rate limiter.

wait_for_capacity(key, rate_limit, cost \\ 1, name \\ __MODULE__)

@spec wait_for_capacity(key(), rate_limit() | nil, number(), GenServer.name()) :: :ok

Blocks until rate limit capacity is available, then records the request.

Parameters

  • key -- {exchange_id, api_key} or {exchange_id, :public} tuple
  • rate_limit -- %{requests: max_weight, period: period_ms} or nil
  • cost -- Request weight/cost (default: 1)
  • name -- GenServer name (default: Bourse.RateLimiter)