Genserver
GenServer patterns for stateful processes in Elixir OTP applications
You are an expert in GenServer design for building robust stateful Elixir applications.
## Key Points
- **Client API**: Public functions that send messages to the server process.
- **Server Callbacks**: `init/1`, `handle_call/3`, `handle_cast/2`, `handle_info/2`, `terminate/2`.
- **State Management**: The server process holds state that is passed through each callback and returned (possibly modified) in the reply tuple.
- **Naming**: Processes can be registered with a name (atom, `{:via, module, term}`, or `{:global, term}`).
- Always define a client API that wraps `GenServer.call/2` and `GenServer.cast/2` — callers should never construct messages directly.
- Use `@impl true` on all callback functions to get compile-time warnings for typos.
- Keep `handle_call` and `handle_cast` callbacks small; delegate heavy work to private functions.
- Use structs or maps with well-defined keys for state rather than bare tuples.
- Set timeouts or use `handle_continue/2` for expensive initialization to avoid blocking the supervisor.
- Prefer `call` over `cast` unless you explicitly need fire-and-forget semantics — `call` provides back-pressure and error propagation.
- **Bottleneck single process**: A single GenServer serializes all requests. If throughput matters, partition state across multiple processes or use ETS for reads.
- **Storing too much state**: Large state in a GenServer increases garbage collection pressure. Offload large data to ETS or persistent storage.skilldb get elixir-phoenix-skills/GenserverFull skill: 165 linesGenServer Patterns — Elixir/Phoenix
You are an expert in GenServer design for building robust stateful Elixir applications.
Overview
GenServer is a behaviour module in OTP that abstracts the common client-server interaction pattern. It provides a process that keeps state, handles synchronous (call) and asynchronous (cast) requests, and integrates with OTP supervision trees.
Core Philosophy
A GenServer is a process that manages state and serializes access to it. This is a powerful primitive, but it is also a bottleneck by design — every call and cast for a given GenServer is processed sequentially. Understanding this tradeoff is essential: GenServers provide safety through serialization, but they become performance constraints if every operation in your system funnels through a single process.
The client-server separation in GenServer is not just an implementation detail; it is a design principle. The client API (public functions) defines what the outside world can do. The server callbacks (handle_call, handle_cast, handle_info) define how the process responds internally. Callers should never construct messages directly or know about the internal state shape. This boundary makes it possible to refactor the server's internals without affecting any calling code.
GenServers should be thin coordination layers, not repositories for business logic. A GenServer that directly queries databases, calls HTTP APIs, and transforms data in its callbacks becomes untestable and fragile. Instead, delegate the actual work to pure functions or context modules, and let the GenServer focus on what it does uniquely well: holding state and serializing access to it.
Anti-Patterns
-
The Singleton Bottleneck: Routing all application requests through a single named GenServer. Since a GenServer processes one message at a time, this serializes your entire application. If throughput matters, partition state across multiple processes or use ETS for read-heavy workloads.
-
Blocking Callbacks: Performing HTTP requests, slow database queries, or file I/O inside
handle_call/3. This blocks all other callers waiting for a response. Spawn a Task for slow work and either reply immediately with an acknowledgment or usehandle_continue/2for deferred initialization. -
Unbounded State Growth: Accumulating data in GenServer state without bounds — storing every event, caching every result, appending to a list indefinitely. This increases garbage collection pressure and memory consumption over time. Set size limits, periodically flush to persistent storage, or use ETS for large datasets.
-
Missing Catch-All handle_info: Omitting a catch-all clause in
handle_info/2. Unexpected messages (from monitors, linked processes, or stale timers) silently accumulate in the mailbox, consuming memory. Always log and discard unrecognized messages. -
Direct Message Construction: Having callers send messages directly with
send/2orGenServer.call(pid, {:some, :internal, :message})instead of going through the client API. This couples callers to the server's internal message format and breaks encapsulation.
Core Concepts
- Client API: Public functions that send messages to the server process.
- Server Callbacks:
init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2. - State Management: The server process holds state that is passed through each callback and returned (possibly modified) in the reply tuple.
- Naming: Processes can be registered with a name (atom,
{:via, module, term}, or{:global, term}).
Implementation Patterns
Basic GenServer
defmodule MyApp.Counter do
use GenServer
# Client API
def start_link(initial_value \\ 0) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
def increment do
GenServer.call(__MODULE__, :increment)
end
def get_value do
GenServer.call(__MODULE__, :get_value)
end
# Server Callbacks
@impl true
def init(initial_value) do
{:ok, initial_value}
end
@impl true
def handle_call(:increment, _from, state) do
new_state = state + 1
{:reply, new_state, new_state}
end
@impl true
def handle_call(:get_value, _from, state) do
{:reply, state, state}
end
end
GenServer with Periodic Work
defmodule MyApp.Poller do
use GenServer
@interval :timer.seconds(30)
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(opts) do
schedule_poll()
{:ok, %{results: [], endpoint: Keyword.fetch!(opts, :endpoint)}}
end
@impl true
def handle_info(:poll, state) do
results = fetch_data(state.endpoint)
schedule_poll()
{:noreply, %{state | results: results}}
end
defp schedule_poll do
Process.send_after(self(), :poll, @interval)
end
defp fetch_data(endpoint) do
# HTTP call or other work
[]
end
end
Dynamic Named Processes with Registry
defmodule MyApp.SessionServer do
use GenServer
def start_link(session_id) do
GenServer.start_link(__MODULE__, session_id, name: via_tuple(session_id))
end
def get_data(session_id) do
GenServer.call(via_tuple(session_id), :get_data)
end
defp via_tuple(session_id) do
{:via, Registry, {MyApp.SessionRegistry, session_id}}
end
@impl true
def init(session_id) do
{:ok, %{session_id: session_id, data: %{}}}
end
@impl true
def handle_call(:get_data, _from, state) do
{:reply, state.data, state}
end
end
Best Practices
- Always define a client API that wraps
GenServer.call/2andGenServer.cast/2— callers should never construct messages directly. - Use
@impl trueon all callback functions to get compile-time warnings for typos. - Keep
handle_callandhandle_castcallbacks small; delegate heavy work to private functions. - Use structs or maps with well-defined keys for state rather than bare tuples.
- Set timeouts or use
handle_continue/2for expensive initialization to avoid blocking the supervisor. - Prefer
callovercastunless you explicitly need fire-and-forget semantics —callprovides back-pressure and error propagation.
Common Pitfalls
- Bottleneck single process: A single GenServer serializes all requests. If throughput matters, partition state across multiple processes or use ETS for reads.
- Storing too much state: Large state in a GenServer increases garbage collection pressure. Offload large data to ETS or persistent storage.
- Blocking in callbacks: Long-running work inside
handle_callblocks all other callers. Spawn a task or usehandle_continueinstead. - Missing
handle_infoclause: Unhandled messages silently accumulate in the mailbox. Always add a catch-allhandle_infothat logs unexpected messages. - Not handling
:timeoutin init: Ifinit/1takes too long, the supervisor may consider the start failed. Use{:ok, state, {:continue, :setup}}for deferred initialization.
Install this skill directly: skilldb add elixir-phoenix-skills
Related Skills
Channels
Phoenix Channels and PubSub for real-time bidirectional communication
Concurrency
Elixir processes and message passing for concurrent and parallel programming
Deployment
Deploying Elixir/Phoenix applications with Mix releases, Docker, and Fly.io
Ecto
Ecto patterns for database schemas, queries, changesets, and migrations in Elixir
Otp Supervision
OTP supervision tree design for building fault-tolerant Elixir applications
Phoenix Liveview
Phoenix LiveView patterns for building real-time, server-rendered interactive UIs