# `HTTPower.Middleware.RateLimiter`
[🔗](https://github.com/mdepolli/httpower/blob/v0.22.0/lib/httpower/middleware/rate_limiter.ex#L1)

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)

# `bucket_key`

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

# `bucket_state`

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

# `rate_limit_config`

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

# `check_rate_limit`

```elixir
@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`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `consume`

```elixir
@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`

```elixir
@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`

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

# `handle_request`

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`

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

Resets a specific bucket, clearing all tokens.

Useful for testing or manual intervention.

# `start_link`

Starts the rate limiter GenServer.

# `update_from_headers`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
