GenServer Basics
Building stateful, message-handling processes the standard way, using OTP's GenServer behaviour instead of raw receive loops.
3 min read
The previous lesson built a stateful process by hand with spawn and a recursive receive loop. That works, but every real Elixir application would be reinventing the same plumbing — starting a process, handling calls and casts, managing state — over and over. GenServer is OTP's standard abstraction for exactly that.
What GenServer gives you
GenServer is a behaviour: a module that defines a set of callback functions you implement, while OTP handles the process lifecycle, message loop, and error handling around them. It's the same pattern (state, handle a message, return new state) from the raw receive loop, just standardized and battle-tested.
A minimal counter
defmodule Counter do
use GenServer
# Client API
def start_link(initial_value) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
def increment do
GenServer.cast(__MODULE__, :increment)
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, state) do
{:noreply, state + 1}
end
@impl true
def handle_call(:value, _from, state) do
{:reply, state, state}
end
end{:ok, _pid} = Counter.start_link(0)
Counter.increment()
Counter.increment()
Counter.value()
# => 2call vs. cast
Every GenServer interaction is one of two kinds:
GenServer.call/2is synchronous — it sends a message and blocks untilhandle_call/3replies. Use it when the caller needs a result back, like readingvalueabove.GenServer.cast/2is fire-and-forget — it sends a message and returns immediately, without waiting for a reply. Use it when the caller doesn't need to wait, likeincrementabove.
Each callback returns a tuple that tells OTP what to do next: handle_call/3 returns {:reply, response, new_state}, while handle_cast/2 returns {:noreply, new_state} since there's no caller waiting for a value.
Client API vs. server callbacks
Notice the module is split into two halves: start_link/1, increment/0, and value/0 are the client API — ordinary functions other code calls. init/1, handle_cast/2, and handle_call/3 are the server callbacks — code that runs inside the GenServer's own process, whenever a message arrives. This separation is a deliberate OTP convention: callers never touch the process's state directly, they only ever send it a message and let the callback decide what happens.
State lives entirely inside the process
The counter's integer state only ever exists inside the GenServer process, exactly like the raw process from the previous lesson — no other process can read or modify it except by sending a message and waiting for handle_call/handle_cast to respond. That's what makes a GenServer safe to call from many places at once: every message is handled one at a time, in the order it arrives, so there's no possibility of two callers corrupting the same state simultaneously.
Why reach for GenServer instead of raw spawn
Anything that needs to hold onto state over time and respond to requests — a cache, a connection pool, a rate limiter, a background job queue — is a GenServer candidate. It gives you the process isolation and message-passing model from the last lesson, wrapped in a standard, well-tested shape that every Elixir developer recognizes on sight. The next lesson, on supervisors, covers what happens when a GenServer crashes — and why, in OTP, that's treated as an expected, recoverable event rather than something to code around defensively.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.