Erlang & Elixir DISCUSSION

GenServer call or cast: which should I use, and what happens when a call times out after 5 seconds?

Started by jorge GenServer call vs castOTP timeoutbackpressureprocess mailboxgen_server
5 replies 248 views 6 participants
Latest activity · 30 Sep 2026

GenServer call or cast: which should I use, and what happens when a call times out after 5 seconds?

jorge Erlang & Elixir Forum
#1

I have a GenServer that owns a serial connection to a measuring instrument. Clients send commands through it. With GenServer.call/2 I sometimes get an exit with a timeout when the instrument is slow; with GenServer.cast/2 there are no errors, but under load the process's memory grows and replies arrive late.

How do I choose between call and cast, and what is the right way to deal with slow operations inside the server?

Community replies 5

Re: GenServer call or cast: which should I use, and what happens when a call times out after 5 seconds?

#2

call is synchronous: the client sends a request and blocks until the server replies from handle_call/3 or until the timeout expires. The default timeout is 5000 ms, and the third argument changes it: GenServer.call(pid, :read, 15_000). On timeout the calling process exits unless it catches the exit. The server is not told; it will still process the request when it gets to it.

cast is asynchronous: it puts a message in the server's mailbox and returns :ok immediately, even if the server is overloaded or no longer exists. The client learns nothing about the outcome. In Erlang the same pair is gen_server:call/3 and gen_server:cast/2.

Re: GenServer call or cast: which should I use, and what happens when a call times out after 5 seconds?

#3

Your memory growth is the missing backpressure of cast. If clients produce commands faster than the instrument handles them, the mailbox grows without limit, because nothing slows the senders down. With call, each client waits for its answer before sending the next request, so the queue can never be longer than the number of clients.

The numbers make it concrete: if one instrument exchange takes 20 ms, the server can complete at most 50 commands per second. Anything above that rate has to be delayed, rejected or dropped somewhere, and cast hides that decision in an ever-growing mailbox. Use call by default and cast only for notifications where losing or delaying one is acceptable.

Re: GenServer call or cast: which should I use, and what happens when a call times out after 5 seconds?

#4

A GenServer processes one message at a time, so a slow handle_call blocks every other client. If the slow part does not need exclusive access, do not do it inside the callback. Return {:noreply, state} from handle_call, keep the from argument, do the work elsewhere (for example in a Task), and answer later with GenServer.reply(from, result). The client still sees an ordinary blocking call.

For a serial instrument, where commands must be strictly one after another anyway, the honest fix is to size the call timeout to the slowest legitimate command plus a margin, and to give the port read its own shorter timeout, so the server returns {:error, :timeout} to the client instead of the client giving up first.

Re: GenServer call or cast: which should I use, and what happens when a call times out after 5 seconds?

#5

You can check whether a server is falling behind with Process.info(pid, :message_queue_len); a value that keeps rising means producers outpace it. From the shell, :sys.get_state(pid) shows the current state while you are debugging.

When many clients only read a value that changes rarely, such as the latest measurement, do not route every read through the server. Let the GenServer write the value into an ETS table and let readers look it up directly; reads then scale across all cores and the server's mailbox only carries the commands that genuinely need the device.

Re: GenServer call or cast: which should I use, and what happens when a call times out after 5 seconds?

#6

Think about failure as part of the design. If the GenServer crashes, its supervisor restarts it and init/1 runs again, so init must be able to reopen the port and rebuild the state; keep it quick and move slow setup into handle_continue. Clients that were blocked in a call at the moment of the crash exit too, which is usually what you want: the failure spreads to the processes that depended on the result, and their supervisors decide what to restart. Wrapping every call in a try and catch that swallows the exit defeats that mechanism.

TEP COMMUNITY