We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Stop blocking your supervisor in `init/1`
almirsarajcic
Every millisecond spent in init/1 is a millisecond your entire supervision tree is frozen. start_link/1 does not return until init/1 does, and the supervisor starts children one at a time — so a cache that loads for 300 ms delays every sibling started after it, and pushes your whole application boot back by the same amount. handle_continue/2 moves that work off the critical path without giving up the guarantee that it runs before any client request.
defmodule PriceCache do
use GenServer
@impl GenServer
def init(opts) do
# ❌ start_link/1 blocks for the full duration of the load
# {:ok, %{rates: load_rates(opts), source: opts[:source]}}
# ✅ return immediately, then keep working
{:ok, %{rates: %{}, source: opts[:source]}, {:continue, :load_rates}}
end
@impl GenServer
def handle_continue(:load_rates, state) do
{:noreply, %{state | rates: load_rates(state.source)}}
end
end
The third element of the :ok tuple is a continue instruction. The process finishes initialising, start_link/1 returns, the supervisor moves on to the next child — and handle_continue/2 runs as the very next thing that process does.
The part that makes it safe
handle_continue/2 is not “run it later and hope”. It is guaranteed to execute before any other message in the mailbox, including messages sent by callers who already have your pid. A client that calls PriceCache.get("EUR") the instant start_link/1 returns will block in GenServer.call/3 until the continue finishes, then get fully loaded state. You get the fast boot without the half-initialised window.
Measured against a 300 ms load:
{blocking, _} = :timer.tc(fn -> Blocking.start_link([]) end)
{continued, _} = :timer.tc(fn -> NonBlocking.start_link([]) end)
IO.puts("init/1 blocking: start_link took #{div(blocking, 1000)}ms")
IO.puts("handle_continue/2: start_link took #{div(continued, 1000)}ms")
# init/1 blocking: start_link took 300ms
# handle_continue/2: start_link took 0ms
The work still takes 300 ms. It just no longer holds the supervisor hostage while it happens.
Chaining and failure
A continue can return another continue, which is how you stage a multi-step startup without a single monster callback:
@impl GenServer
def handle_continue(:connect, state) do
{:noreply, %{state | conn: open_connection(state)}, {:continue, :subscribe}}
end
@impl GenServer
def handle_continue(:subscribe, state) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "rates")
{:noreply, state}
end
Failure semantics stay intact. Crashing inside handle_continue/2 terminates the process and the supervisor restarts it exactly as it would for a crash in init/1 — the difference is that the restart no longer blocks the sibling that was waiting behind you.
One caveat worth knowing: handle_continue/2 runs after init/1 returns, so anything that must be true before the supervisor considers the child started — registering a name, claiming a resource other children depend on — still belongs in init/1. Move the slow, self-contained work; keep the ordering guarantees.
copied to clipboard