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

Installation

Add req_throttle to your list of dependencies in mix.exs:

def deps do
[
{:req_throttle, "~> 0.2.0"}
]
end

Usage

Basic Usage

# 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

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

def deps do
[
{:req_throttle, "~> 0.2.0"},
{:hammer, "~> 7.0"}
]
end

Set up Hammer in your application:

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:

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:

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:

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:

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

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

Key Generators

ReqThrottle provides several pre-configured key generators that can be used as atoms or functions:

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

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:

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

Req.new()
|> ReqThrottle.attach(
rate_limiter: MyApp.RateLimiter,
key_generator: :host
)

Rate Limit by Endpoint

Req.new()
|> ReqThrottle.attach(
rate_limiter: MyApp.RateLimiter,
key_generator: :path
)

Custom Rate Limiting Logic

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.