Using Dynamic Supervision in Elixir for Scalable WebSocket Connections
Learn how to add and remove WebSocket connection processes at runtime with Elixir’s dynamic supervision, including a runnable example and key trade‑offs.
21 Jul 2026, 11:46 UTC

Problem: managing a varying number of WebSocket connections
When a server accepts WebSocket connections, the number of active clients can rise and fall unpredictably. Creating a fixed pool of worker processes at compile time either wastes resources when traffic is low or risks exhausting the system when traffic spikes. Elixir’s supervision tree offers a way to add and remove workers at runtime without restarting the whole supervisor.
Thesis: dynamic supervision lets you start child processes on demand while keeping fault isolation
By using Supervisor.start_child/3 with a :one_for_one strategy, each WebSocket connection runs in its own child process. If a single connection crashes, only that child is restarted (or left stopped, depending on the restart policy), and the supervisor itself continues to manage other connections.
Worked example: a minimal WebSocket connection supervisor
- Create a new Mix project:
mix new ws_demo --sup - Add a simple GenServer that represents a connection (in
lib/ws_demo/connection.ex):defmodule WsDemo.Connection do use GenServer # client‑side API def start_link(_) do GenServer.start_link(__MODULE__, :ok, name: via_tuple()) end defp via_tuple() do {:via, Registry, {WsDemo.Registry, make_ref()}} end # server callbacks def init(:ok) do {:ok, %{}} end def handle_info(:timeout, state) do {:noreply, state} end end - Define a registry to look up connections by a unique reference (in
lib/ws_demo/registry.ex):defmodule WsDemo.Registry do use Registry, keys: :duplicate, names: :local end - Create the supervisor that will dynamically start children (in
lib/ws_demo/supervisor.ex):defmodule WsDemo.Supervisor do use Supervisor def start_link(_) do Supervisor.start_link(__MODULE__, :ok, name: WsDemo.Supervisor) end def init(:ok) do children = [] opts = [strategy: :one_for_one, name: WsDemo.Supervisor] Supervisor.init(children, opts) end # helper to start a new connection child def start_connection() do spec = { WsDemo.Connection, [], [id: WsDemo.Connection, restart: :temporary] } Supervisor.start_child(WsDemo.Supervisor, spec) end end - Start the supervisor in your application (
lib/ws_demo/application.ex):defmodule WsDemo.Application do use Application def start(_type, _args) do children = [WsDemo.Supervisor, WsDemo.Registry] opts = [strategy: :one_for_one, name: WsDemo.Application] Supervisor.start_link(children, opts) end end - When a new WebSocket handshake succeeds, call
WsDemo.Supervisor.start_connection()from your socket module. The supervisor spawns a freshWsDemo.Connectionprocess without restarting itself.
Trade‑offs and limitations
- Runtime overhead: Each call to
start_child/3incurs a small supervision cost. For extremely short‑lived tasks (e.g., fire‑and‑forget messages), aTaskpool or:poolboymay be more efficient. - Restart intensity limits: The supervisor’s
max_restartsandmax_secondsapply to all its children. If many connections fail rapidly, the supervisor itself can shut down, taking down the whole service. Adjust these values in the child spec or use a:transientrestart policy to avoid cascading restarts. - Visibility: Dynamically added children do not appear in the module’s static child list, so tools like
observershow them only after they start. Monitoring must therefore rely on the supervisor’s internal state or a registry.
Actionable closing
If your workload consists of long‑lived, connection‑oriented processes such as WebSocket channels, dynamic supervision gives you the elasticity you need while preserving fault isolation. Start with a :one_for_one supervisor, use :temporary or :transient restart policies, and monitor restart intensity. For high‑frequency, short‑lived jobs, consider a task pool instead.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.