Skip to content

Rate Limiter Service in Elixir

Custom rate limiter service using ETS and GenServer

Untitled-1
defmodule Horionos.Services.RateLimiter do
  @moduledoc """
  Rate limiter service.
  """
  use GenServer
 
  @callback check_rate(String.t(), integer(), integer()) :: :ok | :error
  @table_name :rate_limiter
 
  @spec start_link(any()) :: :ignore | {:error, any()} | {:ok, pid()}
  #
  def start_link(_opts) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end
 
  @spec init(any()) :: {:ok, map()}
  #
  def init(_) do
    :ets.new(@table_name, [:set, :named_table, :public, read_concurrency: true])
    {:ok, %{}}
  end
 
  @doc """
  Checks if the action is allowed based on the given key, limit, and time window.
 
  ## Parameters
 
    - key: A unique identifier for the action (e.g., "confirmation_instructions:user@example.com")
    - limit: The maximum number of actions allowed within the time window
    - window: The time window in milliseconds
 
  ## Returns
 
    - :ok if the action is allowed
    - :error if the rate limit has been exceeded
  """
  @spec check_rate(String.t(), integer(), integer()) :: :ok | :error
  #
  def check_rate(key, limit, window) do
    now = System.system_time(:millisecond)
 
    case :ets.lookup(@table_name, key) do
      [{^key, _count, last_reset}] when now - last_reset > window ->
        :ets.insert(@table_name, {key, 1, now})
        :ok
 
      [{^key, count, _last_reset}] when count >= limit ->
        :error
 
      [{^key, _count, _last_reset}] ->
        :ets.update_counter(@table_name, key, {2, 1})
        :ok
 
      [] ->
        :ets.insert(@table_name, {key, 1, now})
        :ok
    end
  end
end