Managing State and Concurrency with Elixir GenServer
Understand how to implement stateful processes using GenServer, balancing synchronous calls and asynchronous casts to build resilient OTP applications.
29 Sept 2025, 00:06 UTC

The Problem: Managing Shared State in a Concurrent World
In a distributed, concurrent system, you often need a single source of truth for a specific piece of data—such as a user session, a rate limiter, or a game state. However, allowing multiple processes to mutate the same memory simultaneously leads to race conditions and corrupted data.
The solution in Elixir is the GenServer (Generic Server). Instead of sharing memory, you encapsulate the state within a dedicated process. Other processes interact with that state by sending messages, ensuring that state updates happen sequentially and predictably.
Thesis: GenServer as the Foundation of OTP State
By utilizing the GenServer behavior, developers can separate the interface (how the rest of the app requests changes) from the implementation (how the state actually changes). This pattern, combined with OTP supervision, allows for stateful services that are both thread-safe and highly available.
Synchronous vs. Asynchronous Communication
A critical engineering decision when using GenServer is choosing between a call and a cast. This choice dictates how your system handles latency and back-pressure.
- GenServer.call/3 (Synchronous): The caller blocks and waits for a response. Use this when you need a return value or must confirm the operation succeeded before proceeding.
- GenServer.cast/2 (Asynchronous): A "fire-and-forget" message. The caller continues immediately. Use this for telemetry, logging, or updates where the result isn't required for the next step.
Fault Tolerance via Supervision
GenServers are rarely started in isolation. They are typically placed under a Supervisor. If a GenServer crashes due to an unexpected error, the Supervisor restarts it according to a defined strategy (e.g., :one_for_one). This ensures that the stateful service is restored automatically, though the internal state is reset to the init/1 value unless persisted externally.
Worked Example: A Simple Key-Value Store
Below is a practical implementation of a stateful store. This example assumes Elixir 1.15+.
defmodule KVStore do
use GenServer
# Client API
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def put(key, value) do
# Use cast for updates where we don't need a confirmation
GenServer.cast(__MODULE__, {:put, key, value})
end
def get(key) do
# Use call because we need the value returned to the caller
GenServer.call(__MODULE__, {:get, key})
end
# Server Callbacks
@impl true
def init(_opts), do: {:ok, %{}}
@impl true
def handle_cast({:put, key, value}, state) do
{:noreply, Map.put(state, key, value)}
end
@impl true
def handle_call({:get, key}, _from, state) do
{:reply, Map.get(state, key), state}
end
end
How to verify this implementation:
- Run
iex -S mixin your project directory. - Start the server:
KVStore.start_link(). - Store a value:
KVStore.put("api_key", "12345"). - Retrieve the value:
KVStore.get("api_key"). Expected output:"12345".
Trade-offs and Limitations
While GenServer is powerful, it introduces a bottleneck. Because a GenServer processes messages sequentially, a single long-running operation in handle_call or handle_cast will block all other messages in the mailbox. This can lead to timeouts for other callers and increased memory usage as the mailbox grows.
To mitigate this, avoid performing heavy I/O or complex computations directly inside the GenServer. Instead, delegate heavy work to separate task processes or use a pool of workers.
Actionable Closing
When designing your stateful components, follow these three rules to maintain system stability:
- Default to
castfor updates unless you explicitly need to handle a failure or receive a response. - Keep callbacks lean to prevent mailbox congestion and timeouts.
- Always supervise your GenServers to ensure the system recovers from crashes automatically.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.