Managing Process State in Elixir with GenServer: Calls vs. Casts
Learn how to manage process state in Elixir using GenServer, including the critical technical differences between synchronous calls and asynchronous casts to avoid timeouts and mailbox overflows.
24 Jul 2026, 02:54 UTC

The Core Problem: State Persistence in a Stateless Environment
In Elixir, processes are isolated and do not share memory. To maintain a piece of data—such as a user session, a rate limiter, or a hardware connection—across multiple requests, you need a process that lives for a duration and holds that data in its memory. The GenServer (Generic Server) behavior provides a standardized way to implement this client-server pattern.
The primary decision when using a GenServer is choosing between a Call (synchronous) and a Cast (asynchronous). Choosing the wrong one can lead to application deadlocks or memory exhaustion.
Mechanism: Synchronous Calls and Asynchronous Casts
A GenServer manages state by recursively calling its own handler functions. When a message arrives, the GenServer executes a handler and returns a tuple containing the response (if needed) and the new state. This new state is then passed back into the process loop for the next message.
Implementation Example: A Simple Request Counter
The following example implements a counter that tracks requests. It uses handle_call for retrieving the count (where the caller needs the value) and handle_cast for incrementing the count (where the caller doesn't need a confirmation).
defmodule RequestCounter do
use GenServer
# Client API
def start_link(initial_count \\ 0) do
GenServer.start_link(__MODULE__, initial_count, name: :request_counter)
end
def increment do
# Asynchronous: Fire and forget
GenServer.cast(:request_counter, :inc)
end
def get_count do
# Synchronous: Wait for response
GenServer.call(:request_counter, :get)
end
# Server Callbacks
@impl true
def init(initial_count) do
{:ok, initial_count}
end
@impl true
def handle_call(:get, _from, state) do
# Return: {:reply, response, new_state}
{:reply, state, state}
end
@impl true
def handle_cast(:inc, state) do
# Return: {:noreply, new_state}
{:noreply, state + 1}
end
end
Execution and Verification
To verify this behavior, run the following in an iex session:
- Start the server:
RequestCounter.start_link(0) - Increment state:
RequestCounter.increment(). This returns:okimmediately, regardless of whether the server has processed the message. - Retrieve state:
RequestCounter.get_count(). This blocks the current process until the GenServer returns the current integer.
Decision Matrix: Call vs. Cast
| Feature | GenServer.call (Synchronous) | GenServer.cast (Asynchronous) |
|---|---|---|
| Return Value | Returns the value sent by the server. | Always returns :ok immediately. |
| Caller Blocking | Blocks until response or timeout (default 5s). | Does not block. |
| Risk | Can cause timeouts or deadlocks. | Can overflow the process mailbox. |
| Use Case | Fetching data, validating state. | Updating logs, triggering background tasks. |
Critical Limitations and Common Pitfalls
The Blocking I/O Trap
Because a GenServer processes messages sequentially (one at a time), performing a long-running operation—such as a heavy database query or a slow HTTP request—inside handle_call blocks all other requests to that server. If multiple processes call a blocked GenServer, they will all time out, potentially causing a cascading failure across your system.
Mailbox Overflow
Since handle_cast does not provide backpressure (the sender doesn't know if the receiver is overwhelmed), sending casts faster than the GenServer can process them will grow the process mailbox. This consumes memory and increases the latency of subsequent calls. If you expect high-volume updates, consider using ETS (Erlang Term Storage) for state and a GenServer only for coordination.
State Isolation
GenServer state is local to that specific process. If you start ten RequestCounter processes, they each have their own independent count. To share state across processes, you must use a Registry to find specific processes or a shared data store like ETS.
Rollback and Recovery
If a GenServer crashes due to an unhandled exception in a callback, the state is lost. To prevent this, GenServers should be started under a Supervisor. When the Supervisor detects a crash, it restarts the GenServer using the arguments provided to start_link, effectively resetting the state to the initial value.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.