Packages

A Req plugin for rate limiting and throttling HTTP requests to external services. Provides a flexible, pluggable rate limiting solution with support for blocking and error modes.

Current section

Files

Jump to
req_throttle README.md
Raw

README.md

# ReqThrottle
A Req plugin for rate limiting and throttling HTTP requests to external services.
ReqThrottle provides a flexible, pluggable rate limiting solution that works with any rate limiter implementation. It supports both blocking (with retries) and error modes.
## Features
- **Pluggable rate limiters** - Use any module with a `hit/1` function or an anonymous function
- **Flexible key generation** - Rate limit by host, path, URL, or custom logic
- **Two modes**:
- **Blocking mode** (default) - Automatically retries until a slot becomes available
- **Error mode** - Immediately returns an error when the limit is reached
- **Simple interface** - Rate limiters handle their own scale and limit configuration
## Installation
Add `req_throttle` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:req_throttle, "~> 0.1.0"}
]
end
```
## Usage
### Basic Usage
```elixir
# Blocking mode (default) - using atom shortcut
Req.new()
|> ReqThrottle.attach(
rate_limiter: MyApp.RateLimiter,
key_generator: :host
)
|> Req.get("https://api.example.com/data")
```
### Error Mode
```elixir
# Error mode - returns an exception immediately when rate limit is exceeded
Req.new()
|> ReqThrottle.attach(
rate_limiter: MyApp.RateLimiter,
mode: :error
)
|> Req.get("https://api.example.com/data")
```
### Using Hammer
First, add Hammer to your dependencies:
```elixir
def deps do
[
{:req_throttle, "~> 0.1.0"},
{:hammer, "~> 7.0"}
]
end
```
Set up Hammer in your application:
```elixir
defmodule MyApp.RateLimiter do
use Hammer, backend: :ets
end
# In your application supervision tree
defmodule MyApp.Application do
def start(_type, _args) do
children = [
MyApp.RateLimiter
# ... other children
]
Supervisor.start_link(children, strategy: :one_for_one)
end
end
```
Then use it with ReqThrottle:
```elixir
Req.new()
|> ReqThrottle.attach(
rate_limiter: MyApp.RateLimiter,
key_generator: :host
)
|> Req.get("https://api.example.com/data")
```
Note: Hammer's `hit/3` function takes `(key, scale, limit)`, but ReqThrottle only calls it with `hit(key)`. You'll need to create a wrapper that handles the scale and limit configuration:
```elixir
defmodule MyApp.RateLimiter do
use Hammer, backend: :ets
@scale :timer.minutes(1)
@limit 100
def hit(key) do
hit(key, @scale, @limit)
end
end
```
### Using Agent
You can create a simple rate limiter using Agent:
```elixir
defmodule MyApp.SimpleRateLimiter do
use Agent
@scale_ms :timer.minutes(1)
@limit 10
def start_link(_opts) do
Agent.start_link(fn -> %{} end, name: __MODULE__)
end
def hit(key) do
now = System.system_time(:millisecond)
window_start = div(now, @scale_ms) * @scale_ms
Agent.get_and_update(__MODULE__, fn state ->
# Clean up old windows (keep only current window)
cleaned_state =
Enum.filter(state, fn {{_k, w}, _} ->
w >= window_start
end)
|> Map.new()
window_key = {key, window_start}
count = Map.get(cleaned_state, window_key, 0)
if count < @limit do
new_state = Map.put(cleaned_state, window_key, count + 1)
{{:allow, count + 1}, new_state}
else
retry_after = @scale_ms - (now - window_start)
{{:deny, retry_after}, cleaned_state}
end
end)
end
end
```
Then use it:
```elixir
# Start the agent in your supervision tree
MyApp.SimpleRateLimiter.start_link([])
Req.new()
|> ReqThrottle.attach(
rate_limiter: MyApp.SimpleRateLimiter,
key_generator: :host
)
|> Req.get("https://api.example.com/data")
```
### Using Anonymous Functions
You can also pass an anonymous function directly:
```elixir
rate_limiter_fn = fn key ->
# Your custom rate limiting logic
# Must return {:allow, count} or {:deny, retry_after_ms}
{:allow, 1}
end
Req.new()
|> ReqThrottle.attach(rate_limiter: rate_limiter_fn)
|> Req.get("https://api.example.com/data")
```
## Configuration Options
- `rate_limiter` (required) - Module atom or function that implements rate limiting
- Module must have a `hit/1` function: `hit(key) -> {:allow, count} | {:deny, retry_after_ms}`
- Function signature: `(key) -> {:allow, count} | {:deny, retry_after_ms}`
- The rate limiter is responsible for managing its own scale, limit, and all rate limiting logic
- `key_generator` - Atom, function, or MFA tuple to generate the rate limit key from the request
- Default: `:host`
- Atom shortcuts: `:host`, `:path`, `:host_and_path`, `:url`
- Function alternatives: `&ReqThrottle.KeyGenerators.key_by_host/1`, `&ReqThrottle.KeyGenerators.key_by_path/1`, etc.
- MFA tuples: `{ReqThrottle.KeyGenerators, :key_by_host, []}`, etc.
- `mode` - `:block` or `:error` (default: `:block`)
- `:block` - Blocks and retries until a slot becomes available
- `:error` - Immediately returns an error when limit is reached
- `max_retries` - Maximum number of retries in block mode (default: `3`)
## Key Generators
ReqThrottle provides several pre-configured key generators that can be used as atoms or functions:
```elixir
# Using atom shortcuts (recommended)
key_generator: :host
key_generator: :path
key_generator: :host_and_path
key_generator: :url
# Or using functions directly
key_generator: &ReqThrottle.KeyGenerators.key_by_host/1
key_generator: &ReqThrottle.KeyGenerators.key_by_path/1
key_generator: &ReqThrottle.KeyGenerators.key_by_host_and_path/1
key_generator: &ReqThrottle.KeyGenerators.key_by_url/1
```
You can also create custom key generators:
```elixir
custom_key_generator = fn request ->
# Extract user ID from request headers or other logic
user_id = Req.get_header(request, "x-user-id")
"#{user_id}:#{URI.parse(request.url).host}"
end
Req.new()
|> ReqThrottle.attach(
rate_limiter: MyApp.RateLimiter,
key_generator: custom_key_generator
)
```
## Error Handling
When using `mode: :error`, the plugin returns an exception:
```elixir
try do
Req.get!(client, "https://api.example.com/data")
rescue
%ReqThrottle.RateLimitError{} = error ->
# Rate limit exceeded
IO.puts("Rate limit exceeded for key '#{error.key}'. Retry after #{error.retry_after_ms}ms")
end
```
## Examples
### Rate Limit by Host
```elixir
Req.new()
|> ReqThrottle.attach(
rate_limiter: MyApp.RateLimiter,
key_generator: :host
)
```
### Rate Limit by Endpoint
```elixir
Req.new()
|> ReqThrottle.attach(
rate_limiter: MyApp.RateLimiter,
key_generator: :path
)
```
### Custom Rate Limiting Logic
```elixir
custom_rate_limiter = fn key ->
# Check external cache, database, etc.
current_count = get_count_from_cache(key)
scale_ms = :timer.minutes(1)
limit = 50
if current_count < limit do
increment_cache(key, scale_ms)
{:allow, current_count + 1}
else
{:deny, scale_ms}
end
end
Req.new()
|> ReqThrottle.attach(rate_limiter: custom_rate_limiter)
```
## License
Copyright (c) 2025
This library is MIT licensed.