What Is a GenServer?
A GenServer is a behaviour module built into OTP that implements the common pattern of a long-running process holding state and responding to requests, so instead of hand-rolling your own receive loop like the Pinger example, you implement a small set of callbacks and OTP handles the process loop, error reporting, and integration with supervisors for you. Under the hood a GenServer is still an ordinary Elixir process with a mailbox, but Elixir's GenServer wraps it with a documented request/reply protocol and sensible defaults.
Cricket analogy: Rather than every team inventing its own DRS review procedure, the ICC standardized a review protocol that every umpire and captain follows, like Kane Williamson calling for a review in the same three-step process every match, so teams focus on cricket instead of reinventing review logistics.
Client API and the Callback Contract
GenServer.start_link/3 spawns the process, links it to the calling process, and calls your module's init/1 callback to compute the initial state, returning {:ok, pid} once init/1 returns {:ok, state}. Client-facing functions such as get_count/1 are ordinary public functions that wrap GenServer.call/2 or GenServer.cast/2, which is the recommended pattern because it hides the messaging details behind a clean API and lets callers treat the server like a regular module.
Cricket analogy: Calling for a new innings is like GenServer.start_link -- the umpire signals play begins (start_link), the openers walk out per a fixed pre-match routine (init/1), and after that batsmen just call for runs through the non-striker rather than shouting instructions to the whole ground.
defmodule Counter do
use GenServer
# Client API
def start_link(initial_value \\ 0) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
def increment(amount \\ 1) do
GenServer.cast(__MODULE__, {:increment, amount})
end
def value do
GenServer.call(__MODULE__, :value)
end
# Server callbacks
@impl true
def init(initial_value) do
{:ok, initial_value}
end
@impl true
def handle_cast({:increment, amount}, state) do
{:noreply, state + amount}
end
@impl true
def handle_call(:value, _from, state) do
{:reply, state, state}
end
endhandle_call vs handle_cast vs handle_info
handle_call/3 answers synchronous requests: the caller blocks until the callback returns {:reply, response, new_state}, making it the right choice whenever the caller needs a result back, such as reading a counter's current value. handle_cast/2 handles fire-and-forget messages, returning {:noreply, new_state} without ever replying to the sender, which suits updates where the caller doesn't need confirmation, while handle_info/2 catches any message that didn't arrive through call or cast, such as a :DOWN notification from a monitored process or a scheduled Process.send_after/3 tick.
Cricket analogy: Asking the third umpire for a run-out decision is like handle_call -- play pauses until the reply comes back on the big screen, whereas radioing a routine field-placement change to the twelfth man is like handle_cast, sent without waiting for acknowledgment; a sudden rain-delay siren is like handle_info, an event nobody explicitly called for.
defmodule TickLogger do
use GenServer
def start_link(_opts) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
@impl true
def init(_state) do
Process.send_after(self(), :tick, 1_000)
{:ok, 0}
end
@impl true
def handle_info(:tick, count) do
IO.puts("Tick ##{count}")
Process.send_after(self(), :tick, 1_000)
{:noreply, count + 1}
end
endManaging State Across Calls
State in a GenServer is just the fourth argument threaded through every callback and returned again in the next reply tuple, so 'updating state' means computing a new value and returning it in {:reply, ..., new_state} or {:noreply, new_state} -- there's no mutable variable anywhere. Because each callback runs to completion before the next message is processed, GenServer state changes are naturally serialized, giving you the safety of a single mutable variable without needing explicit locks.
Cricket analogy: The scoreboard total isn't a variable anyone edits directly -- after every ball, the scorer computes the new total and writes it down, then the next ball is scored against that freshly written number, one ball strictly after another.
GenServer.call/3 has a default 5-second timeout; if the callback doesn't reply in time, the caller raises an exit -- for long-running work inside handle_call, either increase the timeout explicitly or reply with {:noreply, state} and send the result back later with GenServer.reply/2.
- GenServer is an OTP behaviour that standardizes the stateful-server pattern behind a documented callback contract.
- start_link/3 spawns and links the process, then calls init/1 to establish initial state.
- handle_call/3 answers synchronous requests and must return a {:reply, ...} tuple.
- handle_cast/2 handles asynchronous, fire-and-forget messages with {:noreply, ...}.
- handle_info/2 catches messages that arrive outside the call/cast protocol, like timers or :DOWN notices.
- State is passed as an argument and returned in each callback's reply tuple -- there is no shared mutable variable.
- Wrapping GenServer.call/cast inside public client functions hides messaging details from callers.
Practice what you learned
1. What must a handle_call/3 callback return to reply to the caller?
2. Which callback is used for fire-and-forget messages that don't need a reply?
3. What kind of messages does handle_info/2 handle?
4. How is state represented in a GenServer?
5. What is the default timeout for GenServer.call/2?
Was this page helpful?
You May Also Like
Processes in Elixir
Learn how Elixir's lightweight BEAM processes enable massive concurrency through isolation and message passing.
Supervisors and OTP
Learn how OTP supervisors monitor child processes and restart them automatically to build fault-tolerant systems.
Tasks and Agents
Learn when to reach for Elixir's Task and Agent abstractions instead of writing a full GenServer.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics