Elixir Project: Rate Limited API
A rate limited API is an API that allows only a fixed number of calls per key. Calls past that number are refused until the count is reset.
The fourth call past a limit of three is refused
One key makes four calls against a limit of three. The first three succeed and the fourth is refused.
defmodule RateLimiter do
use GenServer
def start_link(limit), do: GenServer.start_link(__MODULE__, %{limit: limit, counts: %{}}, [])
def allow(pid, key), do: GenServer.call(pid, {:allow, key})
def remaining(pid, key), do: GenServer.call(pid, {:remaining, key})
def init(state), do: {:ok, state}
def handle_call({:allow, key}, _from, state) do
count = Map.get(state.counts, key, 0)
if count < state.limit do
{:reply, :ok, %{state | counts: Map.put(state.counts, key, count + 1)}}
else
{:reply, {:error, :limited}, state}
end
end
def handle_call({:remaining, key}, _from, state), do: {:reply, state.limit - Map.get(state.counts, key, 0), state}
end
{:ok, limiter} = RateLimiter.start_link(3)
IO.inspect(RateLimiter.allow(limiter, :api))
IO.inspect(RateLimiter.allow(limiter, :api))
IO.inspect(RateLimiter.allow(limiter, :api))
IO.inspect(RateLimiter.allow(limiter, :api))
IO.inspect(RateLimiter.remaining(limiter, :api))
GenServer.stop(limiter)Each key has its own count
Two keys share one limiter but count separately. One key reaching its limit leaves the other key free.
defmodule RateLimiter do
use GenServer
def start_link(limit), do: GenServer.start_link(__MODULE__, %{limit: limit, counts: %{}}, [])
def allow(pid, key), do: GenServer.call(pid, {:allow, key})
def init(state), do: {:ok, state}
def handle_call({:allow, key}, _from, state) do
count = Map.get(state.counts, key, 0)
if count < state.limit do
{:reply, :ok, %{state | counts: Map.put(state.counts, key, count + 1)}}
else
{:reply, {:error, :limited}, state}
end
end
end
{:ok, limiter} = RateLimiter.start_link(2)
IO.inspect(RateLimiter.allow(limiter, :a))
IO.inspect(RateLimiter.allow(limiter, :a))
IO.inspect(RateLimiter.allow(limiter, :b))
IO.inspect(RateLimiter.allow(limiter, :a))
IO.inspect(RateLimiter.allow(limiter, :b))
GenServer.stop(limiter)Reset restores a key after refusal
A key hits its limit and is refused. After a reset the same key is allowed again.
defmodule RateLimiter do
use GenServer
def start_link(limit), do: GenServer.start_link(__MODULE__, %{limit: limit, counts: %{}}, [])
def allow(pid, key), do: GenServer.call(pid, {:allow, key})
def reset(pid, key), do: GenServer.call(pid, {:reset, key})
def remaining(pid, key), do: GenServer.call(pid, {:remaining, key})
def init(state), do: {:ok, state}
def handle_call({:allow, key}, _from, state) do
count = Map.get(state.counts, key, 0)
if count < state.limit do
{:reply, :ok, %{state | counts: Map.put(state.counts, key, count + 1)}}
else
{:reply, {:error, :limited}, state}
end
end
def handle_call({:reset, key}, _from, state), do: {:reply, :ok, %{state | counts: Map.delete(state.counts, key)}}
def handle_call({:remaining, key}, _from, state), do: {:reply, state.limit - Map.get(state.counts, key, 0), state}
end
{:ok, limiter} = RateLimiter.start_link(2)
IO.inspect(RateLimiter.allow(limiter, :api))
IO.inspect(RateLimiter.allow(limiter, :api))
IO.inspect(RateLimiter.allow(limiter, :api))
IO.inspect(RateLimiter.reset(limiter, :api))
IO.inspect(RateLimiter.allow(limiter, :api))
IO.inspect(RateLimiter.remaining(limiter, :api))
GenServer.stop(limiter)Example
defmodule RateLimiter do
use GenServer
def start_link(limit), do: GenServer.start_link(__MODULE__, %{limit: limit, counts: %{}}, [])
def allow(pid, key), do: GenServer.call(pid, {:allow, key})
def remaining(pid, key), do: GenServer.call(pid, {:remaining, key})
def init(state), do: {:ok, state}
def handle_call({:allow, key}, _from, state) do
count = Map.get(state.counts, key, 0)
if count < state.limit do
{:reply, :ok, %{state | counts: Map.put(state.counts, key, count + 1)}}
else
{:reply, {:error, :limited}, state}
end
end
def handle_call({:remaining, key}, _from, state), do: {:reply, state.limit - Map.get(state.counts, key, 0), state}
end
{:ok, limiter} = RateLimiter.start_link(2)
handler = fn key ->
case RateLimiter.allow(limiter, key) do
:ok -> "data"
{:error, :limited} -> "limited"
end
end
results = [handler.("user1"), handler.("user1"), handler.("user1")]
IO.inspect(results)
IO.inspect(handler.("user2"))
IO.inspect(RateLimiter.remaining(limiter, "user1"))
GenServer.stop(limiter)Key points
- Limit: the fixed number of calls allowed per key.
- Key: the caller identity that owns its own count.
- Allow: a call that is inside the limit and succeeds.
- Refusal: a call past the limit that returns a limited error.
- Remaining: how many calls a key still has left.
- Reset: clearing a key count so its calls are allowed again.
In this example, a handler checks the limiter before serving data. The third call from the same user is limited, while a new user is still served.
Note
one key hitting its limit never blocks a different key.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
A limiter allows 2 calls per key. Key A makes 2 calls and key B makes 1 call. What happens to the next call from key B?