# Stop blocking your supervisor in `init/1`

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.

```elixir
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:

```elixir
{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:

```elixir
@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.

[`handle_continue/2` docs](https://hexdocs.pm/elixir/GenServer.html#c:handle_continue/2)


---

Created by: almirsarajcic
Date: August 24, 2026
URL: https://elixirdrops.net/d/z1bJQT3c
