`GenServer.call/3` timeout does not cancel work

almirsarajcic

almirsarajcic

1 hour ago

0 comments

Treat the third argument to GenServer.call/3 as a caller deadline, not a cancellation mechanism. When the deadline expires, the call exits the caller while the GenServer continues processing the request.

defmodule PricingClient do
  def quote(server, cart_id) do
    GenServer.call(server, {:quote, cart_id}, 2_000)
  catch
    :exit, {:timeout, {GenServer, :call, _args}} ->
      {:error, :timeout}
  end
end

GenServer.call/2 uses a default timeout of 5,000 milliseconds. GenServer.call/3 accepts a positive integer in milliseconds or :infinity.

A timeout is an exit, not an {:error, :timeout} return value. Catch the full {:timeout, {GenServer, :call, args}} shape only at a boundary where a timeout is an expected result, as above. Other exits—such as calling a missing server—still propagate instead of being mislabeled as timeouts.

The server receives no cancellation signal when the caller stops waiting. Its handle_call/3 callback keeps running, including any external requests or database writes it already started. Because a GenServer handles callbacks sequentially, that work can also keep later messages waiting. Use a task or an asynchronous workflow when the server must remain responsive during slow work.

On OTP 24 and later, gen_server.call uses process aliases, so a reply sent after the timeout is discarded instead of entering the caller’s mailbox. Older OTP releases could leave a {reference, reply} message behind. This prevents mailbox garbage on current OTP, but it still does not stop the server’s work.

GenServer.call/3 also does not link the caller to the server or make the timeout part of a supervision tree. Its guarantee is narrower: wait for a reply up to the configured deadline, then exit the caller.

GenServer.call/3 docs

Comments (0)

Sign in with GitHub to join the discussion