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")
endManual 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:
- Tracks individual request timestamps
- Removes expired entries outside the window
- Counts remaining entries to determine current usage
- 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
@type check_result() :: :ok | {:error, :rate_limited, 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() }
@type rate_limit_key() :: :user_id | :device_id | :ip_address | String.t()
Functions
@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
})
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)
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- TheRequeststruct 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
@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:globalor{service, request_type}tuplerate_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)
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec clear() :: :ok
Clears all rate limit data from ETS tables.
Useful for testing or resetting rate limit counters.
Detaches a telemetry handler by ID.
Gets all configured rate 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}]
@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.
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)
@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:globalor{service, request_type}tuplerate_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)
@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}
])
Starts the RateLimiter GenServer.
@spec update_config(map()) :: :ok
Updates rate limit configuration at runtime.
Parameters
config- A map with:global_limitsand/or:api_limitskeys
Returns
:ok- Configuration was updated
Examples
RateLimiter.update_config(%{
global_limits: [
%{key: :user_id, max_requests: 2000, window_ms: 60_000}
]
})