100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

GenServer Explained

Understand how GenServer standardizes stateful server processes in Elixir with a clean client/callback API.

Concurrency & OTPIntermediate10 min readJul 10, 2026
Analogies

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.

elixir
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
end

handle_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.

elixir
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
end

Managing 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

Was this page helpful?

Topics covered

#Programming#ElixirStudyNotes#GenServerExplained#GenServer#Explained#Client#API#StudyNotes#SkillVeris#ExamPrep