PhoenixGenApi.RateLimiter (PhoenixGenApi v2.11.0)

Copy Markdown View Source

Provides rate limiting functionality for API requests.

This module implements a sliding window rate limiter using ETS for high-performance tracking. It supports both global rate limiting (across all APIs) and per-API rate limiting with configurable limits.

Architecture

The rate limiter uses a sliding window algorithm with ETS tables for storage:

  • Global Table: Tracks request counts per key across all APIs
  • API Table: Tracks request counts per key per API

Rate Limiting Strategies

Global Rate Limiting

Applies a single rate limit across all API requests for a given key. Useful for preventing overall system abuse.

Per-API Rate Limiting

Applies specific rate limits to individual API endpoints. Useful for protecting expensive or sensitive operations.

Configuration

Configure rate limits in your config.exs:

config :phoenix_gen_api, :rate_limiter,
  enabled: true,
  global_limits: [
    # Default: 2000 requests per minute per user
    %{key: :user_id, max_requests: 2000, window_ms: 60_000},
    # Device-level: 10000 requests per minute per device
    %{key: :device_id, max_requests: 10000, window_ms: 60_000}
  ],
  api_limits: [
    # Expensive operation: 10 requests per minute per user
    %{
      service: "data_service",
      request_type: "export_data",
      key: :user_id,
      max_requests: 10,
      window_ms: 60_000
    },
    # Public endpoint: 100 requests per minute per IP
    %{
      service: "public_service",
      request_type: "search",
      key: :ip_address,
      max_requests: 100,
      window_ms: 60_000
    }
  ]

Usage

Basic Usage

# Check rate limit before executing a request
case RateLimiter.check_rate_limit(request) do
  :ok ->
    # Execute the request
    Executor.execute!(request)

  {:error, :rate_limited, details} ->
    # Return rate limit error to client
    Response.error_response(request.request_id, "Rate limit exceeded")
end

Manual Rate Limiting

# Check global rate limit
RateLimiter.check_rate_limit("user_123", :global, :user_id)

# Check API-specific rate limit
RateLimiter.check_rate_limit("user_123", {"my_service", "my_api"}, :user_id)

Rate Limit Keys

The rate limiter supports various key types:

  • :user_id - Rate limit by user
  • :device_id - Rate limit by device
  • :ip_address - Rate limit by IP address
  • Custom keys - Any string value

Sliding Window Algorithm

The rate limiter uses a sliding window algorithm that:

  1. Tracks individual request timestamps
  2. Removes expired entries outside the window
  3. Counts remaining entries to determine current usage
  4. Provides accurate rate limiting without fixed window boundaries

Performance

  • ETS tables provide O(1) average-case lookups
  • Cleanup runs periodically to remove expired entries
  • Memory usage is bounded by max_requests × number of keys
  • Read/write concurrency is enabled for high-throughput scenarios

Fault Tolerance

  • Rate limiter failures do not block request execution (fail-open by default)
  • ETS tables are automatically cleaned up on process termination
  • Configuration changes are applied without restart

Summary

Functions

Adds a single global rate limit at runtime.

Attaches a telemetry handler to rate limiter events.

Checks if a request is within rate limits.

Checks rate limit for a specific key and scope.

Returns a specification to start this module under a supervisor.

Clears all rate limit data from ETS tables.

Detaches a telemetry handler by ID.

Gets all configured rate limits.

Gets the current global rate limits (may differ from config.exs if changed at runtime).

Gets current rate limit status for a key.

Removes a global rate limit by key at runtime.

Resets rate limit counters for a specific key.

Sets (replaces) all global rate limits at runtime.

Starts the RateLimiter GenServer.

Updates rate limit configuration at runtime.

Types

api_identifier()

@type api_identifier() :: {String.t() | atom(), String.t()}

check_result()

@type check_result() :: :ok | {:error, :rate_limited, rate_limit_details()}

rate_limit_details()

@type rate_limit_details() :: %{
  key: String.t(),
  max_requests: non_neg_integer(),
  current_requests: non_neg_integer(),
  window_ms: non_neg_integer(),
  retry_after_ms: non_neg_integer(),
  scope: :global | api_identifier()
}

rate_limit_key()

@type rate_limit_key() :: :user_id | :device_id | :ip_address | String.t()

Functions

add_global_limit(limit)

@spec add_global_limit(map()) :: :ok

Adds a single global rate limit at runtime.

If a limit with the same :key already exists, it will be replaced.

Parameters

  • limit - A map with :key, :max_requests, and :window_ms

Returns

  • :ok - Limit was added

Examples

PhoenixGenApi.RateLimiter.add_global_limit(%{
  key: :ip_address,
  max_requests: 100,
  window_ms: 60_000
})

attach_telemetry(handler_id, function, config \\ %{})

Attaches a telemetry handler to rate limiter events.

Events

  • [:phoenix_gen_api, :rate_limiter, :check] - Emitted on every rate limit check
  • [:phoenix_gen_api, :rate_limiter, :exceeded] - Emitted when a rate limit is exceeded
  • [:phoenix_gen_api, :rate_limiter, :reset] - Emitted when rate limits are reset
  • [:phoenix_gen_api, :rate_limiter, :cleanup] - Emitted during periodic cleanup

Examples

# Attach a handler
:telemetry.attach(
  "my-rate-limiter-handler",
  [:phoenix_gen_api, :rate_limiter, :check],
  fn event, measurements, metadata, config ->
  ...
  end,
  %{}
)

# Or use the helper function
PhoenixGenApi.RateLimiter.attach_telemetry("my-handler", &my_handler/4)

check_rate_limit(request)

Checks if a request is within rate limits.

This function checks both global and per-API rate limits configured for the request. If any limit is exceeded, it returns an error with details.

Parameters

  • request - The Request struct to check

Returns

  • :ok - Request is within all rate limits
  • {:error, :rate_limited, details} - Request exceeds a rate limit

Examples

request = %Request{
  user_id: "user_123",
  device_id: "device_456",
  service: "my_service",
  request_type: "my_api"
}

case RateLimiter.check_rate_limit(request) do
  :ok ->
    # Proceed with request execution

  {:error, :rate_limited, details} ->
    # Return rate limit error
    ...
end

check_rate_limit(key_value, scope, rate_limit_key)

@spec check_rate_limit(String.t(), :global | api_identifier(), rate_limit_key()) ::
  :ok | {:error, :rate_limited, rate_limit_details()}

Checks rate limit for a specific key and scope.

Parameters

  • key_value - The value to rate limit against (e.g., user ID)
  • scope - Either :global or {service, request_type} tuple
  • rate_limit_key - The type of key (:user_id, :device_id, etc.)

Returns

  • :ok - Within rate limit
  • {:error, :rate_limited, details} - Exceeded rate limit

Examples

# Check global rate limit for a user
RateLimiter.check_rate_limit("user_123", :global, :user_id)

# Check API-specific rate limit
RateLimiter.check_rate_limit("user_123", {"service", "api"}, :user_id)

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

clear()

@spec clear() :: :ok

Clears all rate limit data from ETS tables.

Useful for testing or resetting rate limit counters.

detach_telemetry(handler_id)

Detaches a telemetry handler by ID.

get_configured_limits()

@spec get_configured_limits() :: %{global: list(), api: list()}

Gets all configured rate limits.

get_global_limits()

@spec get_global_limits() :: [map()]

Gets the current global rate limits (may differ from config.exs if changed at runtime).

Returns

A list of global rate limit maps.

Examples

PhoenixGenApi.RateLimiter.get_global_limits()
# => [%{key: :user_id, max_requests: 2000, window_ms: 60_000}]

get_rate_limit_status(key_value, scope, rate_limit_key)

@spec get_rate_limit_status(String.t(), :global | api_identifier(), rate_limit_key()) ::
  map()

Gets current rate limit status for a key.

Returns

A map with current usage information for all applicable rate limits.

remove_global_limit(key)

@spec remove_global_limit(atom() | String.t()) :: :ok

Removes a global rate limit by key at runtime.

Parameters

  • key - The rate limit key to remove (:user_id, :device_id, etc.)

Returns

  • :ok - Limit was removed (or didn't exist)

Examples

PhoenixGenApi.RateLimiter.remove_global_limit(:ip_address)

reset_rate_limit(key_value, scope, rate_limit_key)

@spec reset_rate_limit(String.t(), :global | api_identifier(), rate_limit_key()) ::
  :ok

Resets rate limit counters for a specific key.

Parameters

  • key_value - The key value to reset (e.g., user ID)
  • scope - Either :global or {service, request_type} tuple
  • rate_limit_key - The type of key

Returns

  • :ok - Counters were reset

Examples

# Reset all rate limits for a user
RateLimiter.reset_rate_limit("user_123", :global, :user_id)

set_global_limits(limits)

@spec set_global_limits([map()]) :: :ok

Sets (replaces) all global rate limits at runtime.

Parameters

  • limits - A list of global rate limit maps, each with:
    • :key - The rate limit key (:user_id, :device_id, :ip_address, or custom string)
    • :max_requests - Maximum requests allowed in the window
    • :window_ms - Window duration in milliseconds

Returns

  • :ok - Limits were updated

Examples

PhoenixGenApi.RateLimiter.set_global_limits([
  %{key: :user_id, max_requests: 2000, window_ms: 60_000},
  %{key: :device_id, max_requests: 10000, window_ms: 60_000}
])

start_link(opts \\ [])

Starts the RateLimiter GenServer.

update_config(config)

@spec update_config(map()) :: :ok

Updates rate limit configuration at runtime.

Parameters

  • config - A map with :global_limits and/or :api_limits keys

Returns

  • :ok - Configuration was updated

Examples

RateLimiter.update_config(%{
  global_limits: [
    %{key: :user_id, max_requests: 2000, window_ms: 60_000}
  ]
})