Why gen_server?
Writing a raw process that loops on receive, matches messages, tracks state, and replies to callers correctly is easy to get subtly wrong — missing a catch-all clause, forgetting to loop with the new state, or mishandling replies are common bugs. gen_server is an OTP behaviour that provides this receive loop, state threading, and reply mechanism as a well-tested, standardized library, so a developer only has to implement a handful of callback functions describing what should happen for each kind of request.
Cricket analogy: Just as a standardized net-bowling machine setup at every franchise nets session removes the need for each team to reinvent how to feed balls at a batter, gen_server abstracts away the repetitive receive-loop boilerplate so every OTP server process follows the same reliable pattern.
The Callback Contract: init, handle_call, handle_cast, handle_info
A gen_server module implements init/1, which runs once at startup and returns the initial state as {ok, State}; handle_call/3, invoked for synchronous requests sent via gen_server:call/2,3, which must return something like {reply, Reply, NewState}; handle_cast/2, invoked for asynchronous requests sent via gen_server:cast/2, returning {noreply, NewState}; and handle_info/2, invoked for any other message that arrives in the mailbox outside the call/cast protocol, such as a monitor's 'DOWN' message.
Cricket analogy: init/1 is like a team's pre-match warm-up setting the starting XI (initial state); handle_call is answering a direct question from the umpire that needs an immediate ruling reply; handle_cast is signaling a substitution to the dugout without waiting for acknowledgment; handle_info is reacting to an unplanned rain interruption announcement.
-module(counter_server).
-behaviour(gen_server).
-export([start_link/0, increment/0, get_count/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, 0, []).
increment() ->
gen_server:cast(?MODULE, increment).
get_count() ->
gen_server:call(?MODULE, get_count).
init(InitialCount) ->
{ok, InitialCount}.
handle_call(get_count, _From, State) ->
{reply, State, State}.
handle_cast(increment, State) ->
{noreply, State + 1}.
handle_info(_Msg, State) ->
{noreply, State}.
call vs cast: Synchronous vs Asynchronous Requests
gen_server:call/2,3 sends a request and blocks the calling process until a reply arrives or a timeout expires — 5000 milliseconds by default, configurable via a third argument or set to infinity. gen_server:cast/2, by contrast, sends a request and returns immediately without waiting for it to be handled at all, making it appropriate for notifications where the caller doesn't need confirmation. A subtle danger is calling gen_server:call from inside a handle_call callback on a process that, directly or indirectly, calls back into the original caller — this creates a circular wait that hangs both processes until the call times out.
Cricket analogy: Asking the third umpire for a review and waiting on the big screen for the decision before play resumes mirrors gen_server:call's blocking wait for a reply; a captain waving to signal a field change without waiting for confirmation mirrors cast; but if the third umpire tried reviewing their own decision it would freeze the game, like a process calling itself.
Never let handle_call for process A make a gen_server:call to process B if there's any chance B's own handling path calls back into A synchronously — this circular wait will hang both processes until the call timeout fires (default 5000ms), surfacing as a confusing timeout exception far from the real cause.
State Management and Hot Code Upgrades
Every gen_server callback threads the process's state explicitly through its return value — {reply, Reply, NewState}, {noreply, NewState}, and so on — rather than mutating a shared variable, keeping state changes explicit and traceable. Beyond ordinary state updates, gen_server also supports the optional code_change/3 callback, which is invoked during a hot code upgrade to transform a running process's existing state into the shape the new code version expects, allowing the server to keep running — and keep its accumulated state — without ever stopping.
Cricket analogy: A scorer who updates the scoreboard after every single ball, carrying forward the exact same running total into the next delivery, mirrors how gen_server threads state immutably through each callback's return tuple; switching to a new digital scoreboard system mid-match without stopping the game mirrors code_change/3's hot upgrade.
gen_server:call/2 defaults to a 5000ms timeout; pass gen_server:call(Server, Request, Timeout) with an explicit value, or infinity, when an operation is known to take longer — but avoid infinity for calls to servers you don't fully trust, since it removes your ability to fail fast.
- gen_server is an OTP behaviour that abstracts the standard receive-loop pattern for stateful server processes.
- init/1 initializes state; handle_call/3 handles synchronous requests; handle_cast/2 handles asynchronous requests; handle_info/2 handles out-of-band messages.
- gen_server:call/2,3 blocks the caller until a reply arrives or a timeout (default 5000ms) expires.
- gen_server:cast/2 is fire-and-forget and returns immediately without waiting for the server to process it.
- Calling gen_server:call on a process that then calls back into the original caller synchronously can deadlock.
- State is threaded immutably through the return tuples of each callback ({reply, Reply, NewState}, {noreply, NewState}, etc.).
- code_change/3 allows a running gen_server's state to be transformed during a hot code upgrade without stopping the process.
Practice what you learned
1. Which gen_server callback handles a synchronous request that expects a reply?
2. What is the default timeout for gen_server:call/2 if not specified?
3. What return tuple does handle_cast/2 use to update state without replying?
4. Which callback is invoked for messages that arrive outside of call/cast, such as a 'DOWN' monitor message?
5. What is code_change/3 used for?
Was this page helpful?
You May Also Like
Processes and Message Passing
Learn how Erlang's lightweight processes communicate exclusively through asynchronous message passing, forming the foundation of concurrent, fault-tolerant systems.
Supervisors Explained
Learn how OTP supervisors use child specifications and restart strategies to automatically detect and recover from process failures.
Fault Tolerance: Let It Crash
Explore Erlang's 'let it crash' philosophy — why defensive programming is often avoided in favor of fast failure and supervisor-driven recovery.
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