Skip to content

Instantly share code, notes, and snippets.

@clutterstack
Last active January 22, 2026 02:56
Show Gist options
  • Select an option

  • Save clutterstack/97a66b4b7d7d82babf586962e80ade95 to your computer and use it in GitHub Desktop.

Select an option

Save clutterstack/97a66b4b7d7d82babf586962e80ade95 to your computer and use it in GitHub Desktop.
Machine affinity for an Elixir Phoenix application on Fly.io. Near copy of @PJUllrich's solution at https://peterullrich.com/request-routing-and-sticky-sessions-in-phoenix-on-fly. More words at https://clutterstack.com/posts/2025-06-01-liveview-machine-affinity
# Copied from https://peterullrich.com/request-routing-and-sticky-sessions-in-phoenix-on-fly
defmodule UselessMachineWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :useless_machine
import Plug.Conn
def call(conn, opts) do
case UselessMachineWeb.RouteHandler.call(conn, opts) do
%Plug.Conn{halted: true} = conn -> conn # if it was redirected, it gets halted in the plug; leave it halted
conn -> super(conn, opts) # if the plug passed it through, now send it to the default version of call/2
end
end
# ...
end
# Modification of https://peterullrich.com/request-routing-and-sticky-sessions-in-phoenix-on-fly
defmodule UselessMachineWeb.RouteHandler do
use Plug.Builder # saves writing explicit init and stuff
import Plug.Conn
require Logger
@cookie_key "fly-machine-id"
@cookie_ttl 1 * 60 * 1000 # ms
def call(conn, opts) do
conn
|> fetch_query_params()
|> fetch_cookies()
|> handle_conn(opts)
end
def handle_conn(%Plug.Conn{params: params} = conn, _opts) do
machine_id = System.get_env("FLY_MACHINE_ID") # Ullrich puts it in his application config: Application.get_env(:chat, :fly_machine_id)
param_id = Map.get(params, "instance")
path_base = Path.basename(conn.request_path) # I added this so I can exempt health checks that come from localhost
cookie_id = Map.get(conn.req_cookies, @cookie_key, machine_id)
# Logger.info("In RouteHandler, request path is #{conn.request_path}")
# Logger.info("In RouteHandler, param_id is #{param_id}")
# Logger.info("In RouteHandler, cookie_id is #{cookie_id}")
cond do
path_base && path_base == "health" ->
Logger.info("Health endpoint. Carry on as normal.")
conn
# The initial request will contain the Machine ID in a parameter. Check for this first.
# If the param matches the Machine ID, give the client a cookie and let it in.
param_id && param_id == machine_id ->
Logger.info("Correct machine based on parameter #{param_id}. Set cookie and let pass.")
put_resp_cookie(conn, @cookie_key, machine_id, max_age: @cookie_ttl)
# If there's a param but it's the wrong one, try a fly-replay.
param_id && param_id != machine_id ->
Logger.info("Incorrect machine #{machine_id} based on parameter #{param_id}. Redirect.")
redirect_to_machine(conn, param_id)
# If there wasn't a param to check against, check for a cookie that matches the Machine ID.
# Right cookie; carry on
cookie_id && cookie_id == machine_id ->
Logger.info("Correct machine based on cookie #{cookie_id}. Let pass.")
# Logger.info("(request path is #{conn.request_path})")
conn
# No param, wrong cookie; fly-replay
cookie_id && cookie_id != machine_id ->
Logger.info("Incorrect machine #{machine_id} based on cookie #{cookie_id}. Redirect.")
# Logger.info("(request path is #{conn.request_path})")
redirect_to_machine(conn, cookie_id)
# Unlike Ullrich's use case, I have only one path in my router that visitors should access.
# If the initial request doesn't have the Machine ID param, we shoudn't serve any content.
true ->
Logger.info("No parameter or cookie. Send 404.")
conn
|> send_resp(404, "Not found")
|> halt()
end
end
defp redirect_to_machine(conn, requested_machine) do
conn
|> put_resp_header("fly-replay", "instance=#{requested_machine}")
|> send_resp(307, "") # Skip the Phoenix controller text. Note that fly-proxy doesn't care what status or body you send with a fly-replay header.
|> halt()
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment