Erlang/OTP Supervision Trees: Choosing a Restart Strategy That Actually Fits Your Workers
How OTP supervisors restart failed workers: one_for_one vs one_for_all vs rest_for_one, a worked supervisor module with map child specs, intensity/period escalation, and the state-loss and crash-loop mistakes to avoid.
29 Jul 2026, 18:52 UTC

When an Erlang process crashes, the supported recovery mechanism is not try/catch — it is a supervisor. A supervisor is an OTP process that monitors its children and restarts them according to a declared strategy, so a transient failure in one worker does not take down the application. The useful answer up front: default to one_for_one with transient children, pick one_for_all only when children genuinely cannot function without each other, and set intensity/period so that a crash loop escalates instead of restarting forever.
How a supervisor decides what to restart
A supervisor module implements one callback, init/1, which returns {ok, {SupFlags, ChildSpecs}}. SupFlags declares the restart strategy plus two numbers that bound restart frequency: intensity (maximum restarts allowed) and period (the sliding window in seconds). If a child crashes more than intensity times within period, the supervisor gives up and terminates itself, propagating the failure to its own supervisor. That escalation is a feature: it converts an unrecoverable local fault into a decision made higher up the tree.
The three strategies differ only in which children get restarted after one child dies:
one_for_one— restart only the crashed child. Correct when children are independent, e.g., one worker per accepted TCP connection.one_for_all— restart every child. Correct when children are tightly coupled, e.g., a worker plus the dedicated process that owns its ETS table; if the owner dies, the worker's state references are meaningless.rest_for_one— restart the crashed child and every child started after it. Useful for ordered dependencies, e.g., a connection process started first and a protocol handler started second that depends on it.
A worked supervisor module
The following is a complete, minimal supervisor for a pool of independent connection workers. It uses the modern map-based child spec format (supported since OTP 18; older tuple specs still work). This is illustrative code, not something run against a live system here.
-module(conn_sup).
-behaviour(supervisor).
-export([start_link/0, start_worker/1]).
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
start_worker(Socket) ->
supervisor:start_child(?MODULE, [Socket]).
init([]) ->
SupFlags = #{strategy => one_for_one,
intensity => 3,
period => 60},
ChildSpec = #{id => conn_worker,
start => {conn_worker, start_link, []},
restart => transient,
shutdown => 5000,
type => worker,
modules => [conn_worker]},
{ok, {SupFlags, [ChildSpec]}}.Run this from your application code or the shell of a node where the module is loaded; no special privileges are needed beyond being able to start processes. Key decisions in this example:
restart => transientmeans the child is restarted only on an abnormal exit. A worker that finishes its job and exits normally stays dead. Usepermanentfor children that must always exist, andtemporaryfor children that are never restarted.shutdown => 5000gives the child five seconds to run itsterminate/2callback (flushing state, closing sockets) before the supervisor kills it.brutal_killskips that entirely and can corrupt external resources;infinitywithout a well-behaved terminate callback can hang your whole shutdown sequence.intensity => 3, period => 60means four crashes within a minute kill the supervisor. Tune this to your real failure modes, not to suppress symptoms.
Verifying the behavior
You can confirm a supervisor actually restarts children from the Erlang shell (erl, with your modules on the code path):
1> {ok, Sup} = conn_sup:start_link().
2> {ok, Pid} = conn_sup:start_worker(fake_socket).
3> exit(Pid, kill).
4> supervisor:which_children(conn_sup).which_children/1 should list a child with a different pid than Pid — that is the restart working. To test escalation, crash the child repeatedly faster than the intensity allows and confirm the supervisor itself exits with a shutdown reason. Check erl -version and the OTP supervisor documentation for your installed release to confirm default intensity/period values, since defaults have changed across releases.
Limits and common mistakes
Restart does not preserve state. A restarted gen_server starts from init/1 with whatever arguments the child spec provides. If the worker held important state, it must rebuild from a durable source: an ETS table owned by a sibling process (the classic reason to use one_for_all or a separate table owner), a database, or a file. Assuming restart preserves data is the single most common supervision mistake.
Generous limits mask crash loops. Setting intensity to 1000 with a long period turns a deterministic bug into an infinite restart cycle that burns CPU and log volume. A tight crash loop should escalate — that is the signal to page a human.
one_for_all amplifies load. If one child in a group of fifty crashes and all fifty restart, you get a reconnect storm against whatever they talk to. Reach for it only when the children truly share fate.
"Let it crash" is not "never handle errors." Expected, routine failures (a malformed client request) belong in normal error handling in the worker. Supervisors are for unexpected faults. Mixing the two either hides bugs or turns routine noise into restarts.
Finally, supervision composes: supervisors can supervise other supervisors, forming the tree that gives OTP applications their fault-isolation structure. Design the tree so that the blast radius of any single process dying is exactly as large as the state it shares — no larger.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.