HTTPower.Middleware.RateLimiter (HTTPower v0.16.0)

View Source

Token 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:

  1. Each bucket has a maximum capacity (max_tokens)
  2. Tokens are added at a fixed rate (refill_rate)
  3. Each request consumes one or more tokens
  4. 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

bucket_key()

@type bucket_key() :: String.t()

bucket_state()

@type bucket_state() :: {current_tokens :: float(), last_refill_ms :: integer()}

rate_limit_config()

@type rate_limit_config() :: [
  requests: pos_integer(),
  per: :second | :minute | :hour,
  strategy: :wait | :error,
  max_wait_time: pos_integer()
]

Functions

check_rate_limit(bucket_key, config \\ [])

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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

consume(bucket_key, config \\ [])

@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:

  • :ok if 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}

get_bucket_state(bucket_key)

@spec get_bucket_state(bucket_key()) :: bucket_state() | nil

Returns the current state of a bucket.

Returns nil if bucket doesn't exist.

get_info(bucket_key)

@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]
}

handle_request(request, config)

Feature callback for the HTTPower pipeline.

Checks and consumes rate limit tokens for the request.

Returns:

  • :ok if 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

reset_bucket(bucket_key)

@spec reset_bucket(bucket_key()) :: :ok

Resets a specific bucket, clearing all tokens.

Useful for testing or manual intervention.

start_link(opts \\ [])

Starts the rate limiter GenServer.

update_from_headers(bucket_key, rate_limit_info)

@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