Leaking Water

We recently had to optimize API calls for the Shopify API which implements a Leaky Bucket algorithm to limit the amount of calls that you can make with a certain period of time. According to their documentation:

The bucket size is 40 calls (which cannot be exceeded at any given time), with a "leak rate" of 2 calls per second that continually empties the bucket. If your app averages 2 calls per second, it will never trip a 429 error ("bucket overflow").

To test various strategies in optimizing for such an API we built our own implementation in Elixir to simulate their API without having to make calls to their live API:

defmodule LeakyBucket.Bucket do
  @moduledoc """
  Simulates a leaky bucket implementation
  """
  use GenServer

  @initial_amount 0
  @increment_rate 1
  @leak_rate 2
  @leak_interval 500
  @size 40

  # Public

  @doc """
  Increments the amount of water in the bucket
  """
  def increment do
    GenServer.call(__MODULE__, :increment)
  end

  # Private

  @doc """
  Starts Bucket with initial amount
  """
  def start_link() do
    GenServer.start_link(__MODULE__, %{count: @initial_amount}, [name: __MODULE__])
  end

  @doc """
  Initiates the leak counter
  """
  def init(state) do
    send self(), :leak
    {:ok, state}
  end

  @doc """
  Returns an error because bucket will be too full
  """
  def handle_call(:increment, _from, %{count: count}) when (count + @increment_rate) > @size do
    {:reply, :too_many_requests, %{count: count}}
  end

  @doc """
  Increments the amount of water in the bucket
  """
  def handle_call(:increment, _from, %{count: count}) do
    {:reply, count + @increment_rate, %{count: count + @increment_rate}}
  end

  @doc """
  Set count to 0 if count is less than leak rate
  """
  def handle_info(:leak, %{count: count}) when count < @leak_rate do
    Process.send_after(self(), :leak, @leak_interval)
    {:noreply, %{count: 0}}
  end

  @doc """
  Leak amount from bucket
  """
  def handle_info(:leak, %{count: count}) do
    Process.send_after(self(), :leak, @leak_interval)
    {:noreply, %{count: count - @leak_rate}}
  end

end