Supervisors and Fault Tolerance
How OTP supervisors implement "let it crash" -- watching processes and restarting them automatically when they fail.
3 min read
Earlier lessons mentioned Elixir's "let it crash" philosophy without fully explaining the mechanism behind it. That mechanism is the supervisor: a process whose entire job is watching other processes and restarting them when they die.
The core idea
Instead of wrapping every risky operation in defensive error handling, OTP encourages a different strategy: write the "happy path" code, let unexpected failures crash the process outright, and have a supervisor bring it back up in a known-good starting state. A crash becomes a contained, recoverable event instead of a cascading failure — this is the practical payoff of the process isolation covered two lessons ago.
Defining a supervisor
defmodule MyApp.Supervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
children = [
Counter,
{TaskQueue, max_size: 100}
]
Supervisor.init(children, strategy: :one_for_one)
end
endchildren lists the processes (typically GenServers) the supervisor is responsible for. When one of them exits unexpectedly, the supervisor restarts it according to its strategy.
Supervision strategies
:one_for_one— if a child crashes, only that child is restarted. The most common strategy, used when children are independent of each other.:one_for_all— if any child crashes, all children are restarted. Use this when children depend on shared state or assumptions that would be invalid if only one restarted.:rest_for_one— if a child crashes, that child and every child started after it are restarted, since later children may depend on earlier ones.
Supervisor.init(children, strategy: :one_for_one)Picking the right strategy is really a question about dependencies between your processes: independent workers want :one_for_one; a pipeline where each stage depends on the one before it often wants :rest_for_one.
Supervision trees
Supervisors can supervise other supervisors, building a tree:
Application.Supervisor (:one_for_one)
├── Database.Supervisor (:one_for_one)
│ ├── ConnectionPool
│ └── QueryCache
└── Web.Supervisor (:one_for_one)
├── Endpoint
└── SocketHandler
A failure deep in the tree — say, QueryCache crashing — only ripples up as far as its immediate supervisor (Database.Supervisor), which restarts just that one child. The rest of the application, including everything under Web.Supervisor, is completely unaffected and keeps serving requests the entire time.
A GenServer that's allowed to crash
Going back to the Counter GenServer from the previous lesson, imagine one clause has a bug:
@impl true
def handle_call(:divide, _from, state) do
{:reply, 100 / state, state} # crashes if state is 0
endIf state is 0, this raises ArithmeticError and the process crashes. Under a supervisor with :one_for_one, that's not a disaster: the supervisor immediately restarts Counter, running init/1 again and returning it to a known starting state (0, or whatever the initial value was), ready to keep serving requests. No manual recovery code was needed anywhere in handle_call/3 itself.
Why this beats defensive coding
Trying to anticipate every possible failure with try/rescue throughout your codebase is a losing battle — there's always some edge case you didn't think of. Supervisors flip the strategy: assume failures will happen, keep the blast radius small through process isolation, and make recovery automatic and consistent. This is precisely why systems built on OTP have a long track record of extremely high uptime, and it's the single biggest reason companies choose Elixir for services that genuinely cannot afford to go down.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.