Elixir OTP Supervision and Failure Recovery

Elixir runs on the BEAM virtual machine and inherits Erlang's OTP framework, built for telephone switches that had to keep running through hardware faults and software bugs. Its central idea is surprising to developers from other ecosystems: instead of trying to handle every possible error defensively, let a failing process crash and have a supervisor restart it in a known-good state. Isolated, lightweight processes make this safe — one crash affects only that process and whatever is explicitly linked to it.

"Let it crash" is not "ignore errors". It requires deliberate design: which processes depend on which, how they restart, where state lives so a restart does not lose it, how to stop crash loops, and how to protect processes from overload. This page builds a small supervised service and shows how restarts, state recovery, and backpressure work in practice.

TL;DR

Quick Example

A supervision tree with a repository, a registry, and a dynamic supervisor for per-warehouse processes:

A GenServer that rebuilds its state after every (re)start and writes durably before replying:

Seeing recovery in iex:

Core Concepts

Processes, Links, and Monitors

BEAM processes are cheap (thousands to millions per node), share no memory, and communicate by messages. A link connects two processes so that if one exits abnormally, the other receives an exit signal and — unless it traps exits — dies too. A monitor is one-directional: the monitoring process receives a :DOWN message and keeps running. Supervisors use links to know when children fail; ordinary code uses monitors to watch processes it depends on without sharing their fate.

Supervisors and Strategies

Order children so dependencies start first. Nest supervisors to create failure domains: a crash loop in one subsystem should not restart unrelated ones.

Restart Intensity and Escalation

If children restart more than max_restarts times within max_seconds (default 3 in 5 seconds), the supervisor gives up and exits, escalating to its own supervisor. This stops infinite crash loops — for example a process that crashes on startup because a database is down — and lets higher levels restart a larger part of the system or, ultimately, the application.

Child Specs and Restart Types

DynamicSupervisor starts children on demand (per user, per connection, per warehouse); Registry maps names to processes; Task.Supervisor runs supervised asynchronous work.

State Recovery

A restarted process begins with whatever init/1 builds — in-memory state is gone. Decide where truth lives:

Write durably before acknowledging work, so a crash after replying cannot lose committed changes.

Backpressure and Timeouts

A process handles one message at a time; if messages arrive faster than it handles them, its mailbox grows until memory runs out. Defenses:

See Quotas, Fairness, and Backpressure.

Shutdown

Supervisors stop children in reverse start order, giving each a shutdown timeout. Processes that must clean up (flush buffers, close connections) trap exits and implement terminate/2 — but terminate is not guaranteed on brutal kills, so durability cannot depend on it. See Backend Service Lifecycle and Graceful Shutdown.

Testing Failure Behavior

Use start_supervised/1 in ExUnit so processes are cleaned up between tests, kill processes deliberately, and assert on recovered state:

Best Practices

Design the Supervision Tree Deliberately

Group processes into failure domains, order dependencies, and choose strategies that reflect real relationships.

Keep Truth Outside Crashing Processes

Persist important state before acknowledging it and rebuild process state on restart.

Let Unexpected Errors Crash

Handle expected errors explicitly; let unexpected ones crash and rely on supervision, rather than wrapping everything in defensive code.

Bound Every Mailbox

Use timeouts, backpressure, and partitioning so no process can be overwhelmed silently.

Common Mistakes

Rescuing Everything

Slow Work in init

Casts Without Backpressure

FAQ

What does "let it crash" mean in Elixir?

Instead of handling every unexpected error defensively, allow a failing process to exit and rely on a supervisor to restart it in a known-good state, keeping failures isolated.

What is the difference between a link and a monitor?

A link is bidirectional and propagates exits, so linked processes fail together unless one traps exits. A monitor is one-directional and delivers a message when the monitored process exits.

Which supervisor strategy should I use?

:one_for_one for independent children, :rest_for_one when later children depend on earlier ones, and :one_for_all when all children must restart together.

Does a GenServer keep its state after a crash?

No. A restarted GenServer starts from init. Keep important state in durable storage or ETS tables owned by longer-lived processes, and rebuild it on restart.

How do I prevent crash loops?

Use restart intensity limits so repeated failures escalate to a higher supervisor, fix the cause, and make startup resilient to temporarily unavailable dependencies.

Related Topics

References