Erlang Supervision Strategies: Choosing one_for_one or rest_for_one by Dependency
Supervision strategy is a claim about which Erlang processes depend on which. Pick it from the dependency graph, then confirm it with a deliberate kill on a test node.
15 Aug 2025, 14:55 UTC

A supervisor that restarts the right process and leaves the wrong ones running
Picture a node that accepts TCP connections. One process owns the listening socket; a pool of acceptor processes block in gen_tcp:accept/1 on that socket. Now suppose the listener crashes and its supervisor is configured with one_for_one. The listener restarts and opens a fresh socket on the same port. The acceptors are untouched — still blocked in accept/1 on the old socket handle, which is now closed. They do not crash. They just wait. The node reports a healthy listener and accepts nothing.
Every individual process is behaving correctly, which is why this is easy to miss. A supervisor restart strategy is a claim about which children depend on which. Choose it from the dependency graph rather than from habit, or you get trees that look right and recover wrong.
What the three static strategies actually promise
| Strategy | When a child terminates abnormally | Fits when |
|---|---|---|
one_for_one | Only that child is restarted. | Children share no state and no ordering. |
rest_for_one | That child and every child started after it are restarted. | Later children hold references to earlier ones — sockets, ETS tables, registered names. |
one_for_all | All children are terminated and restarted. | Children are mutually dependent, or the group is only valid as a whole. |
Two details matter more than the table. "After" means position in the child list returned from init/1 — start order, not crash order. And restart type interacts with strategy: a child declared restart => temporary is never restarted by its supervisor, so it cannot trigger a sibling restart under rest_for_one.
Worked example: a TCP acceptor pool
The dependency here is one-directional: acceptors need the listener's socket; the listener does not need the acceptors. That is the shape rest_for_one was built for. The child order in init/1 encodes the edge.
-module(acceptor_pool_sup).
-behaviour(supervisor).
-export([start_link/1, init/1]).
start_link(Port) ->
supervisor:start_link({local, ?MODULE}, ?MODULE, Port).
init(Port) ->
SupFlags = #{strategy => rest_for_one,
intensity => 5,
period => 10},
Listener = #{id => listener,
start => {acceptor_pool_listener, start_link, [Port]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [acceptor_pool_listener]},
Acceptors = #{id => acceptors,
start => {acceptor_pool_acceptors, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [acceptor_pool_acceptors]},
{ok, {SupFlags, [Listener, Acceptors]}}.
The acceptors child would itself be a supervisor or a process that owns the acceptor pool; the point is that it starts second. With this ordering, a listener crash restarts the listener and the acceptors, so they re-accept on the new socket. An acceptor crash restarts only the acceptors, leaving the listening socket intact — which is what you want. The modules key is metadata used by release handling to know which modules a child runs; it does not affect restarts. intensity => 5, period => 10 means at most five restarts in ten seconds before the supervisor gives up and terminates itself.
What it costs, and how to check it
rest_for_one is not free: a listener crash tears down every acceptor, dropping in-flight connections and any per-connection state they held. That is usually the correct trade — a stale socket handle is worse than a reconnection — but it is a real cost, and one_for_all would be strictly worse here because it would also restart children that had nothing to do with the failure.
Two further limits. First, the strategy is read only when the supervisor starts. Editing and reloading the module does not change a running supervisor; the new strategy applies when that supervisor process next restarts. There is no runtime call that flips it in place. Second, restart intensity is a blast radius control, not a tolerance setting: set it from how long a downstream dependency stays broken, not from how many crashes you are willing to ignore, because tripping it terminates the supervisor and hands the decision to its parent.
Verifying the behavior on a test node
- Start the application on a test node and attach a shell to the same node, for example
erl -sname pooltest. Runobserver:start()to draw the supervision tree in the Applications tab. Observer needs a working GUI; on a headless host, skip it and use the shell calls below. - Inspect the supervisor's state:
sys:get_status(acceptor_pool_sup).This returns child specs and current PIDs. No special permissions are needed beyond being able to message the process. - Record the current children:
supervisor:which_children(acceptor_pool_sup). - Kill the listener:
exit(whereis(acceptor_pool_listener), kill).Thekillsignal is untrappable, so the child gets no chance to clean up and the supervisor sees an abnormal exit. Repeat step 3. Underrest_for_oneboth the listener and the acceptors should show new PIDs; underone_for_oneonly the listener's PID changes. - To see the limit, kill the listener six times inside ten seconds. Expect a supervisor report about reaching the maximum restart intensity, followed by the supervisor terminating.
Run these steps only on a test node: killing processes drops in-flight connections and whatever state they held. Also check that whereis/1 returns a PID at all — if the child is not registered under the name you expect, the call returns undefined and you will be killing nothing.
If you changed the strategy and need to go back, revert the module and restart the supervisor's subtree or the node. Reloading the code alone will not restore the previous behavior, because the running supervisor still holds the old flags.
Before picking a strategy, write down the dependency edges between children. "Acceptors need the listener's socket" is an edge; "two independent caches" is not. The strategy then follows from the graph, and one deliberate kill in a test node tells you whether you read the graph correctly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.