HTTPower.Middleware.RateLimiter (HTTPower v0.16.0)
View SourceToken bucket rate limiter for HTTPower.
Implements a token bucket algorithm to enforce rate limits on HTTP requests. Each bucket refills tokens at a configured rate, and requests consume tokens.
Features
- Token bucket algorithm with automatic refill
- Per-client or per-endpoint rate limiting
- Configurable strategies: wait or error
- ETS-based storage for high performance
- Automatic cleanup of old buckets
- Support for custom bucket keys
Configuration
config :httpower, :rate_limit,
enabled: true, # Enable/disable rate limiting (default: false)
requests: 100, # Max requests per time window
per: :second, # Time window: :second, :minute, :hour
strategy: :wait, # Strategy: :wait or :error
max_wait_time: 5000, # Max wait time in ms (default: 5000)
adaptive: true # Adjust rate based on circuit breaker health (default: true)Usage
# Global rate limiting (from config)
HTTPower.get("https://api.example.com/users")
# Per-client rate limiting
client = HTTPower.new(
base_url: "https://api.example.com",
rate_limit: [requests: 50, per: :minute]
)
HTTPower.get(client, "/users")
# Custom bucket key
HTTPower.get("https://api.example.com/users",
rate_limit_key: "api.example.com"
)Token Bucket Algorithm
The token bucket algorithm works as follows:
- Each bucket has a maximum capacity (max_tokens)
- Tokens are added at a fixed rate (refill_rate)
- Each request consumes one or more tokens
- If no tokens available:
- :wait strategy - waits until tokens are available (up to max_wait_time)
- :error strategy - returns {:error, :too_many_requests}
Adaptive Rate Limiting
When adaptive: true is enabled, rate limits automatically adjust based on
circuit breaker health to prevent thundering herd during service recovery:
- Circuit closed (healthy) → 100% rate (full speed)
- Circuit half-open (recovering) → 50% rate (be gentle)
- Circuit open (down) → 10% rate (minimal health checks)
This coordination prevents overwhelming a recovering service with full traffic immediately after it comes back up.
Implementation Details
- Uses ETS table for fast in-memory storage
- Tokens refill continuously based on elapsed time
- Buckets are automatically cleaned up after inactivity
- Thread-safe with atomic check-and-consume via GenServer serialization
- Adaptive mode queries circuit breaker state (read-only, no coupling)
Summary
Functions
Checks if a request can proceed under rate limit constraints.
Returns a specification to start this module under a supervisor.
Consumes tokens from the bucket and waits if necessary.
Returns the current state of a bucket.
Returns rate limit information for a bucket.
Feature callback for the HTTPower pipeline.
Resets a specific bucket, clearing all tokens.
Starts the rate limiter GenServer.
Updates bucket state from server rate limit headers.
Types
@type bucket_key() :: String.t()
@type rate_limit_config() :: [ requests: pos_integer(), per: :second | :minute | :hour, strategy: :wait | :error, max_wait_time: pos_integer() ]
Functions
@spec check_rate_limit(bucket_key(), rate_limit_config()) :: {:ok, float()} | {:ok, :disabled} | {:error, :too_many_requests, integer()}
Checks if a request can proceed under rate limit constraints.
Returns:
{:ok, remaining_tokens}if request is allowed{:error, :too_many_requests, wait_time_ms}if rate limit exceeded{:ok, :disabled}if rate limiting is disabled
Examples
iex> HTTPower.RateLimiter.check_rate_limit("api.example.com")
{:ok, 99.0}
iex> HTTPower.RateLimiter.check_rate_limit("api.example.com", requests: 5, per: :second)
{:error, :too_many_requests, 200}
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec consume(bucket_key(), rate_limit_config()) :: :ok | {:error, :too_many_requests}
Consumes tokens from the bucket and waits if necessary.
This is the main function used by HTTPower.Client. It handles both :wait and :error strategies.
Returns:
:okif request can proceed{:error, :too_many_requests}if rate limit exceeded and strategy is :error{:error, :too_many_requests}if wait time exceeds max_wait_time
Examples
iex> HTTPower.RateLimiter.consume("api.example.com")
:ok
iex> HTTPower.RateLimiter.consume("api.example.com", strategy: :error)
{:error, :too_many_requests}
@spec get_bucket_state(bucket_key()) :: bucket_state() | nil
Returns the current state of a bucket.
Returns nil if bucket doesn't exist.
@spec get_info(bucket_key()) :: map() | nil
Returns rate limit information for a bucket.
Includes both current token count and server-provided limits if available.
Returns nil if bucket doesn't exist.
Examples
iex> HTTPower.RateLimiter.get_info("api.github.com")
%{
current_tokens: 55.0,
last_refill_ms: 1234567890,
server_limit: 60,
server_remaining: 55,
server_reset_at: ~U[2025-10-01 12:00:00Z]
}
Feature callback for the HTTPower pipeline.
Checks and consumes rate limit tokens for the request.
Returns:
:okif request can proceed{:error, reason}if rate limit exceeded
Examples
iex> request = %HTTPower.Request{url: "https://api.example.com", ...}
iex> HTTPower.RateLimiter.handle_request(request, [requests: 100, per: :minute])
:ok
@spec reset_bucket(bucket_key()) :: :ok
Resets a specific bucket, clearing all tokens.
Useful for testing or manual intervention.
Starts the rate limiter GenServer.
@spec update_from_headers(bucket_key(), map()) :: :ok
Updates bucket state from server rate limit headers.
This synchronizes the local bucket with the server's actual rate limit state. Server headers provide: limit, remaining, reset_at timestamp. We convert this to our token bucket format: remaining tokens + current time.
Examples
iex> rate_limit_info = %{
...> limit: 60,
...> remaining: 55,
...> reset_at: ~U[2025-10-01 12:00:00Z],
...> format: :github
...> }
iex> HTTPower.RateLimiter.update_from_headers("api.github.com", rate_limit_info)
:ok