Managing State in Elixir: When to Use Call vs. Cast in GenServer
Stop blocking your Elixir processes. Learn when to use GenServer.call versus GenServer.cast to balance system responsiveness with state consistency.
29 Aug 2026, 01:54 UTC

The Bottleneck of Synchronous State
In a concurrent system, the biggest risk isn't usually a crash—it's a deadlock or a timeout. When using Elixir's GenServer (a behavior for implementing client-server relationships), developers often default to GenServer.call/3 because it provides an immediate answer. However, relying solely on synchronous calls creates a tight coupling between the sender and the receiver. If the server is busy processing a heavy request, every other process waiting for a response hangs, potentially triggering a cascade of timeouts across your application.
The goal is to move as much work as possible to asynchronous patterns without losing the ability to verify that critical state changes actually happened.
Synchronous Calls (call) for Queries and Critical Updates
A call is a blocking operation. The calling process pauses execution until the GenServer processes the message and sends a reply. This is essential for two scenarios: retrieving the current state (queries) and operations where the caller cannot proceed without confirmation of success.
Because the BEAM (the Erlang Virtual Machine) handles these as messages, a call includes a built-in timeout (defaulting to 5 seconds). If the GenServer's mailbox is backed up, the caller will crash with a timeout error, which is a signal that your server is overloaded or stuck in a blocking I/O operation.
Asynchronous Casts (cast) for Fire-and-Forget Updates
A cast is non-blocking. The caller sends the message and immediately continues its own execution. The GenServer processes the message whenever it reaches the front of the mailbox. This is ideal for telemetry, logging, or updating state where the result doesn't change the caller's immediate logic.
The trade-off is visibility. Since a cast provides no response, you cannot know if the operation failed or if the server crashed during processing. If you cast a message to a process that has died, the message is simply dropped.
Worked Example: A Simple Rate Limiter
Consider a rate limiter that tracks how many requests a user has made. We use call to check if a user is allowed to proceed and cast to increment the counter, as the caller doesn't need to wait for the increment to finish to move on.
defmodule RateLimiter do
use GenServer
# Client API
def start_link(initial_state \ %{}) do
GenServer.start_link(__MODULE__, initial_state, name: :rate_limiter)
end
def check_limit(user_id) do
# Synchronous: We MUST know the limit before proceeding
GenServer.call(:rate_limiter, {:check, user_id})
end
def increment_count(user_id) do
# Asynchronous: We don't need to wait for the DB/State update to return
GenServer.cast(:rate_limiter, {:increment, user_id})
end
# Server Callbacks
def init(state), do: {:ok, state}
def handle_call({:check, user_id}, _from, state) do
count = Map.get(state, user_id, 0)
{:reply, count < 100, state}
end
def handle_cast({:increment, user_id}, state) do
new_state = Map.update(state, user_id, 1, &(& + 1))
{:noreply, new_state}
end
end
The Danger of the Growing Mailbox
While cast prevents the caller from blocking, it introduces a different risk: mailbox overflow. Since cast never blocks, a fast producer can send messages faster than the GenServer can process them. This leads to increased memory pressure on the BEAM and delayed processing of critical call messages, as they must wait behind thousands of pending casts.
To verify the health of your GenServer, you can check the process mailbox size in IEx using :process.message_queue_len(pid). If this number grows consistently, you have a producer-consumer mismatch that no amount of cast usage can fix; you may need to shard your state across multiple GenServers using a Registry.
Decision Matrix
| Scenario | Recommended Method | Risk |
|---|---|---|
| Fetching a value | call |
Timeout if server is busy |
| Updating a counter | cast |
Mailbox buildup |
| Critical state transition | call |
Increased latency for caller |
| Triggering a background task | cast |
Silent failure on crash |
Actionable Summary
When designing your GenServer, start by identifying which operations are "queries" and which are "commands." Use call for queries and critical commands. Use cast for non-critical updates. If you find your system timing out, check your mailbox lengths; if they are high, avoid adding more casts and instead look into distributing the load across multiple processes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.