How to Monitor Phoenix.Endpoint with Vigilmon
Phoenix.Endpoint is where every HTTP request and WebSocket connection enters your Phoenix application. It handles SSL termination, static file serving, session cookies, LiveView socket connections, and the full plug pipeline before a request reaches your router. Configuring it correctly is straightforward; detecting when it breaks in production is not.
Endpoint failures are some of the hardest to diagnose because they can be partial: the router works but WebSocket upgrades fail; SSL is valid but the certificate is about to expire; the endpoint starts but binds to the wrong interface after a VM restart. Vigilmon monitors the endpoint from outside — the way your users see it — so you know when something breaks before they do.
Why Monitor Phoenix.Endpoint?
The endpoint is the front door. When it's misconfigured or unavailable, your entire application is down:
- Port binding failure — after a crash and restart, a stale PID holds the port; the supervisor starts the endpoint process but it can't bind, so the app is unreachable
- SSL certificate expiry — Certbot renews fail silently; your certificate expires and browsers block access before you notice
- WebSocket handshake failures — the HTTP router returns 200 but LiveView socket upgrades return 403 or fail the upgrade, breaking real-time features
- Static asset serving broken — a misconfigured
:httpvs:httpsscheme causes asset URLs to mismatch, breaking CSS and JS after a domain change - Endpoint not in supervision tree — a refactor accidentally removes
MyAppWeb.Endpointfromapplication.ex; the app starts but the HTTP server never comes up
Key Metrics to Track
| Metric | What it tells you |
|--------|-------------------|
| HTTP availability | Whether the endpoint is accepting connections and returning expected responses |
| Response time | Whether request handling is within acceptable latency bounds |
| SSL certificate validity | Days remaining before expiry; Vigilmon alerts before browsers start blocking |
| WebSocket endpoint reachability | Whether /live/websocket or /socket/websocket accepts upgrade requests |
| Health check keyword | Whether the correct application version is serving (catches wrong-version deploys) |
| Heartbeat freshness | Whether the endpoint's critical background workers ran recently |
Step 1: Add a Health Check Plug
Phoenix.Endpoint plugs run before the router, so a health plug can respond without touching your authentication middleware or database connection pool — important for liveness checks:
# lib/my_app_web/plugs/endpoint_health.ex
defmodule MyAppWeb.Plugs.EndpointHealth do
import Plug.Conn
def init(opts), do: opts
def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
checks = %{
database: check_database(),
endpoint: :ok,
version: app_version()
}
all_ok? = checks |> Map.delete(:version) |> Enum.all?(fn {_, v} -> v == :ok end)
status = if all_ok?, do: 200, else: 503
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(%{status: status_text(status), checks: checks}))
|> halt()
end
def call(conn, _opts), do: conn
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
{:error, _} -> :error
end
end
defp app_version do
Application.spec(:my_app, :vsn) |> to_string()
end
defp status_text(200), do: "ok"
defp status_text(_), do: "degraded"
end
Register the plug in your endpoint, before the router and before session middleware:
# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
# Health check responds before any auth or session processing
plug MyAppWeb.Plugs.EndpointHealth
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [session: @session_options]]
plug Plug.Static, at: "/", from: :my_app, gzip: false,
only: MyAppWeb.static_paths()
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"], json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug MyAppWeb.Router
end
Test it:
curl -s http://localhost:4000/health | jq .
# {
# "status": "ok",
# "checks": {
# "database": "ok",
# "endpoint": "ok",
# "version": "0.1.0"
# }
# }
Step 2: Configure Endpoint Telemetry
Phoenix.Endpoint already emits [:phoenix, :endpoint, :stop] telemetry for every request. Attach a handler to track slow requests:
# lib/my_app/endpoint_telemetry.ex
defmodule MyApp.EndpointTelemetry do
require Logger
@slow_threshold_ms 2_000
def attach do
:telemetry.attach(
"my-app-endpoint-slow-requests",
[:phoenix, :endpoint, :stop],
&__MODULE__.handle_stop/4,
nil
)
end
def handle_stop(_event, %{duration: duration}, %{conn: conn}, _config) do
ms = System.convert_time_unit(duration, :native, :millisecond)
if ms > @slow_threshold_ms do
Logger.warning(
"[Endpoint] Slow request: #{conn.method} #{conn.request_path} took #{ms}ms",
path: conn.request_path,
method: conn.method,
duration_ms: ms,
status: conn.status
)
end
end
end
Attach in application start:
# lib/my_app/application.ex
def start(_type, _args) do
MyApp.EndpointTelemetry.attach()
# ...
end
Step 3: Add a WebSocket Liveness Check
LiveView relies on the WebSocket endpoint. Verify it's accessible as part of your deployment check:
# lib/my_app_web/plugs/endpoint_health.ex — extend checks
defp check_websocket_path do
# The LiveView socket path should return a 403 or 200 for HTTP GET
# (WS upgrades happen in the browser; HTTP GET to the socket path is valid for probing)
case :httpc.request(:get, {~c"http://localhost:4000/live/websocket", []}, [], []) do
{:ok, {{_, status, _}, _, _}} when status in [200, 400, 403, 426] ->
# 400/403/426 means the socket path exists but refused HTTP (expected for WS-only endpoints)
:ok
{:ok, {{_, 404, _}, _, _}} ->
:error
{:error, _} ->
:error
end
end
Add it to the checks map:
checks = %{
database: check_database(),
endpoint: :ok,
websocket: check_websocket_path(),
version: app_version()
}
Step 4: Set Up Monitoring in Vigilmon
HTTP Monitor
- Sign in at vigilmon.online
- Click New Monitor → HTTP
- URL:
https://yourdomain.com/health - Expected status code:
200 - Add a keyword check for
"endpoint":"ok"in the response body - Check interval: 1 minute
SSL Certificate Monitor
- Click New Monitor → SSL
- Enter your domain:
yourdomain.com - Alert threshold: 14 days before expiry
Vigilmon's SSL monitor checks daily and sends an alert 14 days before the certificate expires — enough time to investigate Certbot renewal issues without any urgency.
WebSocket Endpoint Monitor
Point a second HTTP monitor at your socket path:
- Click New Monitor → HTTP
- URL:
https://yourdomain.com/live/websocket - Expected status: anything except
404(Vigilmon treats 400/403/426 as "endpoint exists") - Add keyword monitor if your WS endpoint returns a body
This catches the case where your router works but the socket configuration is broken after a deploy.
Step 5: Alerting
In Vigilmon, go to Notifications → New Channel → Slack:
Endpoint down:
🔴 DOWN: yourdomain.com/health
Status: 503 Service Unavailable
Keyword "endpoint":"ok" not matched
Action: Check Phoenix.Endpoint supervisor process and port binding
SSL expiry approaching:
⚠️ SSL EXPIRY: yourdomain.com
Certificate expires in 12 days
Action: Verify Certbot auto-renewal timer (systemctl status certbot.timer)
WebSocket endpoint degraded:
🔴 DOWN: yourdomain.com/live/websocket
Status: 404 Not Found
Action: Check Phoenix.Endpoint socket configuration and LiveView socket path
Configure the Slack channel on all three monitors. Each fires independently, so you know whether a failure is an HTTP issue, an SSL issue, or a WebSocket-specific problem.
Step 6: Status Page and Badge
In Vigilmon:
- Go to Status Pages → New Status Page
- Add all three monitors (HTTP health, SSL, WebSocket)
- Name it
yourdomain.com Status - Copy the public URL
Share the status page URL in your team's Slack channel topic so everyone checks it first when users report issues.
README badge:

What You Built
| What | How |
|------|-----|
| Health check plug | EndpointHealth plug responds before router and session middleware |
| DB + version checks | Ecto ping and Application.spec/2 in health response |
| WebSocket path probe | HTTP liveness check of /live/websocket path |
| Slow request logging | Telemetry handler on [:phoenix, :endpoint, :stop] |
| HTTP monitor | Vigilmon checks /health with keyword match every minute |
| SSL certificate monitor | Vigilmon alerts 14 days before certificate expiry |
| WebSocket monitor | Second HTTP monitor on the LiveView socket path |
| Status page | Vigilmon public status page for all three monitors |
| Slack alerting | Vigilmon Slack notification channel |
Phoenix.Endpoint is the single most critical process in your application. Vigilmon watches it from the outside, so you know immediately when it fails to serve users.
Next Steps
- Add a response-time baseline to your Vigilmon HTTP monitor to detect slow request trends before they become outages
- Create a Vigilmon monitor for each additional region if you deploy to multiple Fly.io or Render regions
- Track
[:phoenix, :endpoint, :stop]duration percentiles in your APM to correlate with DB or LiveView performance - Set a Vigilmon alert for keyword mismatch on
"version"in the health response to catch wrong-version deploys
Get started free at vigilmon.online.