tutorial

How to Monitor Phoenix.Endpoint with Vigilmon

Add production-grade uptime monitoring to your Phoenix.Endpoint — track HTTP availability, WebSocket health, SSL expiry, and endpoint configuration drift across deploys.

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 :http vs :https scheme causes asset URLs to mismatch, breaking CSS and JS after a domain change
  • Endpoint not in supervision tree — a refactor accidentally removes MyAppWeb.Endpoint from application.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

  1. Sign in at vigilmon.online
  2. Click New Monitor → HTTP
  3. URL: https://yourdomain.com/health
  4. Expected status code: 200
  5. Add a keyword check for "endpoint":"ok" in the response body
  6. Check interval: 1 minute

SSL Certificate Monitor

  1. Click New Monitor → SSL
  2. Enter your domain: yourdomain.com
  3. 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:

  1. Click New Monitor → HTTP
  2. URL: https://yourdomain.com/live/websocket
  3. Expected status: anything except 404 (Vigilmon treats 400/403/426 as "endpoint exists")
  4. 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:

  1. Go to Status Pages → New Status Page
  2. Add all three monitors (HTTP health, SSL, WebSocket)
  3. Name it yourdomain.com Status
  4. 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:

![Uptime](https://vigilmon.online/badge/your-monitor-slug)

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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →