Shutting down a Phoenix app when idle

A neon sign of a man unplugging a cord from a giant electrical outlet
Image by Annie Ruygt

Edit - Apr 14th 2023
Since the writing of this post, we’ve rolled out V2/machines apps on Fly.io. V2/machine apps are now the default for new organizations. And we encourage existing ones to create new V2/machine apps. Which mean you can use this with your new apps!

This is a quick post about how you can get a Phoenix app to shut itself down from within if nobody’s using it. On Fly.io, this makes the most sense if the app is running on Fly Machine VMs. Check them out!

If you’ve ever wanted to shut down an application when no one’s connected to it, say for demand-driven horizontal scaling using Fly Machines, then we have the perfect little recipe for you. At the time of writing, “regular” Fly apps don’t run on Machines, so you can’t use this to scale.

When a Fly Machine is in a stopped state, you’re not using RAM or CPU, so you pay only for storage. It becomes entirely reasonable to boot a Phoenix app per user to serve their requests. If you’re wondering why you might do such a thing, stay tuned ;)

To unlock this feature in an Elixir app, simply add a task to your supervision tree that checks periodically for active connections and shuts down the Erlang runtime if it finds there are none:

# lib/my_app/application.ex

  def start(_type, _args) do 
    children = [
      ...,
      AppWeb.Endpoint,
      {Task, fn -> shutdown_when_inactive(:timer.minutes(10)) end},
    ]
    opts = [strategy: :one_for_one, name: App.Supervisor]
    Supervisor.start_link(children, opts)
  end

  defp shutdown_when_inactive(every_ms) do
    Process.sleep(every_ms)
    if :ranch.procs(AppWeb.Endpoint.HTTP, :connections) == [] do
      System.stop(0)
    else
      shutdown_when_inactive(every_ms)
    end
  end

Within an app’s start/2 function, Supervisor.start_link(children, opts) spawns the top-level supervisor and all the child processes in the list of children.

We can pass a function directly into our supervision tree using the Task module, without having to wrap things in an extra GenServer process. Here, we use it to pass an anonymous function that runs our custom shutdown code:

{Task, fn -> shutdown_when_inactive(:timer.minutes(10)) end}

We implement a private function, shutdown_when_inactive/1, that sleeps for a given interval before checking the web server’s connection pool library, Ranch, for any active HTTP connections. If there are none, it calls System.stop(0) to gracefully shut down the VM. Otherwise, it calls itself, starting the next sleep interval before it checks again. Lather, rinse, repeat.

When a Fly Machine VM’s main process exits, that VM enters a stopped state, but it isn’t destroyed. The Fly.io platform proxy will try to wake up a stopped machine automatically in response to incoming connection requests, so with this recipe we can build a scale-on-demand Phoenix app!

Since HTTPS for our application on Fly.io is terminated at the load balancer, we don’t need to worry about HTTPS connections. For those applications that accept both HTTP and HTTPS, you’ll need to check the :ranch.procs(AppWeb.Endpoint.HTTPS, :connections) for active HTTPS processes as well.

Fly.io ❤️ Elixir

Fly.io is a great way to run your Phoenix LiveView app close to your users. It’s really easy to get started. You can be running in minutes.

Deploy a Phoenix app today!