Choosing Between GenServer and Spawned Processes for Erlang State Management
Deciding between GenServer and spawned processes in Erlang involves balancing overhead against operational stability. This guide compares both for state management.
19 Jul 2026, 14:39 UTC

The State Management Dilemma
When building a concurrent system in Erlang, you must decide how to encapsulate state. While every Erlang process can maintain state through recursive function calls, the choice between using a raw spawned process and an OTP gen_server determines how your system handles failures, scales, and communicates with other components. The primary trade-off is between minimal overhead and operational predictability.
Comparison of State Encapsulation Methods
| Feature | Spawned Process (Manual) | GenServer (OTP) |
|---|---|---|
| Overhead | Minimal; no wrapper logic | Slightly higher due to behavior wrapper |
| Communication | Asynchronous only (!) |
Sync (call) and Async (cast) |
| Lifecycle | Manual termination/tracking | Standardized start/stop/restart |
| Fault Tolerance | Manual link/monitor setup | Native Supervisor integration |
| Boilerplate | Low; just a recursive function | Moderate; requires callback module |
Decision Constraints and Trade-offs
Use a spawned process when the task is short-lived, transient, or requires a non-standard messaging pattern. If you are creating thousands of ephemeral workers that perform a single calculation and exit, the overhead of a gen_server behavior is unnecessary. However, manual processes lack a standardized way to request data back from the process without manually managing receive blocks and unique reference tags.
Use a GenServer when the process represents a long-lived entity (e.g., a user session, a database connection pool, or a configuration manager). The gen_server module provides a predictable interface for other processes to query the state synchronously. This prevents the "fire and forget" ambiguity of raw messaging.
Risk Warning: Synchronous gen_server:call operations can introduce deadlocks. If Process A calls Process B, and Process B calls Process A before returning, both processes will hang indefinitely. In high-throughput systems, prefer cast (asynchronous) unless a return value is strictly required for the next step of execution.
Implementation: State Management Comparison
Assume we are building a simple counter. Below is the implementation for both approaches. These examples assume Erlang/OTP 25+.
Option A: The Spawned Process (Manual)
-module(manual_counter).
-export([start/0, loop/1]).
start() ->
spawn(fun() -> loop(0) end).
loop(Count) ->
receive
increment ->
loop(Count + 1);
{get_count, From} ->
From ! {count, Count},
loop(Count)
end.
Option B: The GenServer (OTP)
-module(otp_counter).
-behaviour(gen_server).
-export([start_link/0, init/1, handle_call/3, handle_cast/2]).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
{ok, 0}.
handle_call({get_count, _From}, _From, Count) ->
{reply, Count, Count}.
handle_cast(increment, Count) ->
{noreply, Count + 1}.
Validation and Diagnostics
To verify the behavior and resource impact, run these commands in the Erlang shell (erl). You will need the modules compiled in your path.
1. Functional Verification:
For the GenServer, use otp_counter:start_link(), then send a cast via gen_server:cast(otp_counter, increment). Verify the state with gen_server:call(otp_counter, {get_count, self()}). The expected result is the current integer value of the counter.
2. Memory Diagnostic:
To check the memory footprint of your processes, use the erlang:memory() function or erlang:process_info(Pid, memory). While a gen_server uses more memory than a raw process, the difference is typically negligible (a few hundred bytes) unless you are spawning millions of processes.
3. Fault Recovery Check:
If the otp_counter is started under a supervisor, kill the process using exit(whereis(otp_counter), kill). Observe the system logs; the supervisor will automatically restart the process, resetting the state to the init/1 value (0). A spawned process killed in this manner will remain dead unless you have manually implemented a monitoring loop.
Rollback and Cleanup
Since these operations change the state of the Erlang VM by creating processes, clean up your environment to avoid memory leaks during testing:
- For GenServers:
gen_server:stop(otp_counter). - For spawned processes:
exit(Pid, kill).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.