Elixir Fundamentals: Functional Programming and OTP · Processes, OTP and Fault Tolerance

GenServer and Supervisors: OTP in Practice

Keep state in a GenServer with call (synchronous) and cast (asynchronous) messages, start it under a Supervisor, and watch the supervisor restart it after a crash. This is the backbone of every Elixir and Phoenix app.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A rate-limited shopping cart service: a GenServer holding carts in its state, a public API that hides the message passing, and a Supervisor that restarts the service if it crashes.

The idea

OTP is the set of libraries and design principles that come with Erlang/Elixir. Two building blocks matter most:

  • GenServer: a process that holds state and handles messages through callbacks:
    • init/1 sets the initial state,
    • handle_call/3 answers synchronous requests (GenServer.call: the caller waits for a reply),
    • handle_cast/2 handles asynchronous ones (GenServer.cast: fire and forget),
    • handle_info/2 handles other messages (timers, monitors).
  • Supervisor: starts child processes and restarts them according to a strategy (:one_for_one restarts only the crashed child) when they crash.

Wrap the message passing in a client API (Cart.add(item)), so callers never see call/cast directly. Name the process (name: __MODULE__) so it can be found without passing pids.

In a Mix project, your application's supervision tree is started in lib/my_app/application.ex; Phoenix apps are trees of supervisors and GenServers.

The code

A GenServer under a Supervisor, surviving a crash
defmodule Cart do
  use GenServer

  # Client API: what the rest of the app calls
  def start_link(_opts), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  def add(user, item), do: GenServer.cast(__MODULE__, {:add, user, item})
  def items(user), do: GenServer.call(__MODULE__, {:items, user})
  def crash, do: GenServer.cast(__MODULE__, :crash)

  # Server callbacks: run inside the GenServer process
  @impl true
  def init(state) do
    IO.puts("cart service started")
    {:ok, state}
  end

  @impl true
  def handle_cast({:add, user, item}, carts) do
    {:noreply, Map.update(carts, user, [item], &[item | &1])}
  end

  def handle_cast(:crash, _carts), do: raise("unexpected state!")

  @impl true
  def handle_call({:items, user}, _from, carts) do
    {:reply, carts |> Map.get(user, []) |> Enum.reverse(), carts}
  end
end

Logger.configure(level: :none)   # keep the crash report out of this demo's output

{:ok, sup} = Supervisor.start_link([Cart], strategy: :one_for_one)

Cart.add("ana", "coffee")
Cart.add("ana", "mug")
Cart.add("ben", "tea")
IO.inspect(Cart.items("ana"), label: "ana")
IO.inspect(Cart.items("ben"), label: "ben")

old_pid = Process.whereis(Cart)
Cart.crash()
Process.sleep(50)                 # give the supervisor a moment to restart it
new_pid = Process.whereis(Cart)

IO.puts("restarted with a new pid: #{old_pid != new_pid}")
IO.inspect(Cart.items("ana"), label: "ana after restart (state was reset)")
Cart.add("ana", "beans")
IO.inspect(Cart.items("ana"), label: "ana")
IO.inspect(Supervisor.count_children(sup), label: "children")

Output

cart service started
ana: ["coffee", "mug"]
ben: ["tea"]
cart service started
restarted with a new pid: true
ana after restart (state was reset): []
ana: ["beans"]
children: %{active: 1, workers: 1, supervisors: 0, specs: 1}

Run this on your computer — see “Get Started” for setup.

The restart resets the state: that's the point (a clean, known-good state), and also why important data belongs in a database, not only in process memory.

Common mistakes

  • Doing slow work inside handle_call, which blocks every caller of that GenServer; offload to Tasks.
  • One GenServer for everything, which becomes a bottleneck; partition state (one process per cart, a Registry, ETS tables).
  • cast when you need confirmation. Use call if the caller must know it succeeded.
  • Keeping critical data only in process state. Persist it; supervisors restart processes with fresh state.
  • Unsupervised processes that silently disappear when they crash.

Key points

  • GenServer = a stateful process with init, handle_call (sync), handle_cast (async), handle_info.
  • Wrap calls in a client API; name processes to find them.
  • Supervisors restart crashed children (:one_for_one and other strategies).
  • Persist important data; process state is a cache that can be reset.

Further reading: GenServer, Supervisor, Supervision trees and applications.

Practice questions

What's the difference between GenServer.call and GenServer.cast?

Answer

call is synchronous: the caller waits (with a default 5-second timeout) for handle_call's reply. cast is asynchronous: it sends the message and returns :ok immediately, handled by handle_cast without a reply.

What does a one_for_one supervisor do when a child crashes?

Answer

It restarts only the crashed child (with fresh state from init), leaving other children running. Other strategies like one_for_all and rest_for_one restart groups of children.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?