Choosing Erlang’s one_for_one Supervision Strategy for Fault‑Tolerant Services
Learn how the one_for_one restart strategy isolates failures in Erlang OTP supervision trees, see a concrete gen_server example, and understand when to use it.
03 Mar 2026, 10:19 UTC

The problem: a crashing worker should not bring down its peers
\nIn a service built with Erlang/OTP you often run several independent workers under a common supervisor. When one worker fails due to a transient error, you want the supervisor to restart only that worker while leaving the others untouched, preserving their state and avoiding unnecessary churn.
\nThesis: one_for_one gives isolated restarts for independent children
\nThe one_for_one restart strategy tells an OTP supervisor to restart exactly the child that terminated, leaving all other children running. This matches the mental model of isolated services and keeps the supervision tree lightweight.
How OTP supervision works
\nA supervisor starts child specifications defined in its init/1 callback. Each spec includes the child module, start function, restart type, and shutdown intensity. The supervisor monitors the children and, upon a crash, applies its restart strategy.
Key behaviours:
\n- \n
gen_server(orgen_statem,gen_event) provides a standard lifecycle:start_link/0,init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2. \n- The supervisor does not need to know the internals of the child; it only reacts to the exit signal. \n
Worked example: two gen_servers, one fails transiently
\nWe define a simple supervisor that starts two workers:
\n- \n
worker_a– agen_serverthat echoes messages. \nworker_b– agen_serverthat simulates a transient error on the third call. \n
Supervisor module (demo_sup.erl):
-module(demo_sup).\n-behaviour(supervisor).\n-export([start_link/0, init/1]).\n\nstart_link() ->\n supervisor:start_link({local, ?MODULE}, ?MODULE, []).\n\ninit([]) ->\n RestartStrategy = {one_for_one, 5, 60}, % 5 restarts per 60 s before giving up\n ChildSpecs = [\n #{id => worker_a,\n start => {worker_a, start_link, []},\n restart => permanent,\n shutdown => 5000,\n type => worker,\n modules => [worker_a]},\n #{id => worker_b,\n start => {worker_b, start_link, []},\n restart => permanent,\n shutdown => 5000,\n type => worker,\n modules => [worker_b]}\n ],\n {ok, {RestartStrategy, ChildSpecs}}.\n\nWorker A (worker_a.erl):
-module(worker_a).\n-behaviour(gen_server).\n-export([start_link/0, init/1, handle_call/3, handle_cast/2, handle_info/2,\n terminate/2, code_change/3]).\n\nstart_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).\n\ninit(_) -> {ok, 0}.\n\nhandle_call({ping, From}, _State, _) ->\n {reply, pong, _State};\nhandle_call(_Request, State, _) -> {reply, ok, State}.\nhandle_cast(_Msg, State) -> {noreply, State}.\nhandle_info(_Info, State) -> {noreply, State}.\nterminate(_Reason, _State) -> ok.\ncode_change(_OldVsn, State, _Extra) -> {ok, State}.\n\nWorker B (worker_b.erl) that crashes on the third call:
-module(worker_b).\n-behaviour(gen_server).\n-export([start_link/0, init/1, handle_call/3, handle_cast/2, handle_info/2,\n terminate/2, code_change/3]).\n\nstart_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).\n\ninit(_) -> {ok, 0}.\n\nhandle_call({ping, From}, State, _) when State < 2 ->\n {reply, pong, State + 1};\nhandle_call({ping, From}, _State, _) ->\n % Simulate transient failure\n exit(transient_error);\nhandle_call(_Request, State, _) -> {reply, ok, State}.\nhandle_cast(_Msg, State) -> {noreply, State}.\nhandle_info(_Info, State) -> {noreply, State}.\nterminate(_Reason, _State) -> ok.\ncode_change(_OldVsn, State, _Extra) -> {ok, State}.\n\nTo build and run:
\n# Compile all modules\n$ erlc demo_sup.erl worker_a.erl worker_b.erl\n# Start an Erlang node and launch the supervisor\n$ erl -sname demo -setcookie demo -s demo_sup start_link\n\nIn the shell you can interact:
\n1> worker_a:ping().\npong\n2> worker_b:ping().\npong\n3> worker_b:ping().\npong\n4> worker_b:ping().\n** exception exit: transient_error\n5> worker_b:ping().\npong % worker_b has been restarted, state reset to 0\n\nThe supervisor’s console output (via error_logger) shows a restart report for worker_b only; worker_a remains alive and retains its state.
Trade‑off and limitation
\nWhile one_for_one isolates failures, it relies on the supervisor’s restart intensity. If a child crashes more than MaxR times within MaxT seconds, the supervisor gives up and shuts down the whole tree. This protects against a runaway failure loop but can also hide a bug that causes frequent crashes. Therefore:
- \n
- Set
MaxRandMaxTconservatively based on observed failure rates. \n - Log each crash (e.g., with
logger:info/2) to detect patterns. \n - Combine automatic restarts with health‑checks or circuit‑breaker patterns for critical workloads. \n
Actionable closing
\nWhen designing a supervision tree:
\n- \n
- Identify whether children are truly independent (no shared state that must survive a sibling’s crash). \n
- If yes, start with
one_for_oneand tuneMaxR/MaxTto your expected error frequency. \n - Verify by simulating a crash (as in the example) and checking that only the failed child restarts. \n
- Monitor the supervisor’s logs; adjust intensity if you see frequent “child terminated” messages without a tree shutdown. \n
By matching the restart strategy to the fault domain, you keep the system resilient while avoiding unnecessary restarts that could mask deeper issues.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.