Gleam OTP Supervisors
A supervisor watches over child processes and restarts them when they crash. Supervisors are the backbone of fault-tolerant systems on the BEAM. Instead of letting one crash bring down the whole application, supervisors contain failures and recover automatically.
The Supervisor Philosophy
"Let it crash" + Supervisor = Resilient System
──────────────────────────────────────────────────
Without supervisor:
Worker crashes → whole app crashes
With supervisor:
Worker crashes → supervisor restarts it → app keeps running
Erlang systems achieve high uptime not by preventing all crashes, but by recovering from them automatically. A supervisor makes that recovery happen without human intervention.
Adding the Dependency
gleam add gleam_otpSupervisor Tree Concept
Application Supervisor Tree
──────────────────────────────────────────────────
App Supervisor
/ | \
WebSup CacheSup DbSup
/ \ | |
Router Worker Cache Pool
Supervisors form a tree. Each node supervises its children. A crash at any leaf is contained and restarted without affecting siblings or the parent supervisor.
Starting a Supervisor
import gleam/otp/supervisor
import gleam/otp/actor
// Define a child worker
fn start_logger() {
actor.start("Logger ready", fn(msg, state) {
io.println("LOG: " <> msg)
actor.continue(state)
})
}
// Build the supervisor
pub fn start_app() {
supervisor.start(fn(children) {
children
|> supervisor.add(supervisor.worker(start_logger))
})
}
Restart Strategies
Child Restart Options
──────────────────────────────────────────────────
supervisor.worker(start_fn)
→ Restarts when the child crashes abnormally
→ Does NOT restart on normal exit
→ Default choice for stateless workers
supervisor.worker_with_restart(start_fn, restart)
→ restart = supervisor.Permanent (always restart)
→ restart = supervisor.Transient (restart on crash)
→ restart = supervisor.Temporary (never restart)
Supervisor Restart Policies
Supervisor-Level Policies
──────────────────────────────────────────────────
one_for_one (default)
→ Only the crashed child restarts
→ Other children continue running
→ Best for independent workers
one_for_all
→ If one child crashes, ALL children restart
→ Use when children depend on each other
rest_for_one
→ If child N crashes, N and all children
after N restart in order
→ Use for ordered dependency chains
Multiple Children
import gleam/otp/supervisor
pub fn start_services() {
supervisor.start(fn(children) {
children
|> supervisor.add(supervisor.worker(start_cache))
|> supervisor.add(supervisor.worker(start_api))
|> supervisor.add(supervisor.worker(start_scheduler))
})
}
Service Supervisor Tree
──────────────────────────────────────────────────
services_supervisor
/ | \
Cache API Scheduler
│ │ │
actor actor actor
If API crashes → supervisor restarts API only (one_for_one)
Cache and Scheduler keep running.
Nested Supervisors
Supervisors can supervise other supervisors, building layered fault isolation:
pub fn start_root() {
supervisor.start(fn(children) {
children
|> supervisor.add(supervisor.supervisor(start_web_layer))
|> supervisor.add(supervisor.supervisor(start_data_layer))
})
}
fn start_web_layer() {
supervisor.start(fn(children) {
children
|> supervisor.add(supervisor.worker(start_router))
|> supervisor.add(supervisor.worker(start_static_server))
})
}
fn start_data_layer() {
supervisor.start(fn(children) {
children
|> supervisor.add(supervisor.worker(start_db_pool))
|> supervisor.add(supervisor.worker(start_cache))
})
}
Nested Supervisor Tree
──────────────────────────────────────────────────
Root Supervisor
/ \
Web Supervisor Data Supervisor
/ \ / \
Router Static DB Pool Cache
A crash in DB Pool only restarts DB Pool.
Web layer is completely unaffected.
Maximum Restart Intensity
If a child crashes too many times in a short period, the supervisor itself stops. This prevents an infinite crash-restart loop from consuming all resources:
Default intensity: 1 crash per 5 seconds
If exceeded: supervisor stops and reports to its own supervisor
This propagates up the tree until a supervisor handles it
or the root supervisor shuts down the app.
Practical Example — Chat Server Supervision
import gleam/otp/supervisor
pub fn start_chat_server() {
supervisor.start(fn(children) {
children
// Message broker — restart on crash
|> supervisor.add(supervisor.worker(start_message_broker))
// Session manager — restart on crash
|> supervisor.add(supervisor.worker(start_session_manager))
// Presence tracker — restart on crash
|> supervisor.add(supervisor.worker(start_presence_tracker))
// HTTP listener — restart on crash
|> supervisor.add(supervisor.worker(start_http_listener))
})
}
pub fn main() {
case start_chat_server() {
Ok(_supervisor) -> {
io.println("Chat server started")
process.sleep_forever() // keep main alive
}
Error(reason) -> {
io.println("Failed to start: " <> debug_reason(reason))
}
}
}
Chat Server Recovery Scenario
──────────────────────────────────────────────────
t=0: All 4 services running
t=120: presence_tracker crashes (memory issue)
t=120: supervisor detects crash
t=121: supervisor restarts presence_tracker
t=122: presence_tracker running again
Users connected to message_broker and session_manager
experienced zero interruption.
Key Points
Supervisor Essentials
──────────────────────────────────────────────────
1. supervisor.start(fn(children) { ... })
2. supervisor.add(children, supervisor.worker(start_fn))
3. Default strategy: one_for_one (only crashed child restarts)
4. Supervisors form trees — crashes are contained locally
5. Maximum intensity prevents infinite restart loops
6. Nested supervisors give layered fault isolation
7. "Let it crash" + supervisors = high-availability systems
Supervisors transform individual processes into resilient systems. You write simple workers that do one thing well and crash loudly when something goes wrong. Supervisors handle the recovery. This clean separation — workers focus on logic, supervisors handle failures — is the secret behind Erlang's legendary reliability record.
