Phoenix LiveView lets you build rich, real-time user interfaces without writing JavaScript — server-rendered HTML flows over a persistent WebSocket connection, delivering the responsiveness of a single-page app with the simplicity of server-side code. But WebSocket-heavy architectures introduce failure modes that traditional HTTP monitoring misses: a LiveView process can crash silently, a reconnect storm after a deploy can overwhelm the node, or an upstream connection pool can exhaust while the HTTP layer stays green. Vigilmon gives you the external observability layer your LiveView stack needs: HTTP uptime checks, WebSocket endpoint monitoring, heartbeat monitors for PubSub subscribers and background workers, and instant alerts to email or Slack.
What You'll Build
- A
/healthendpoint covering HTTP, database, and PubSub connectivity - A Vigilmon HTTP monitor for the LiveView WebSocket upgrade path
- A heartbeat monitor for GenServer-based LiveView data feeds
- Alert channels (email and Slack)
- A public status page for your users
Prerequisites
- A Phoenix LiveView app (Phoenix 1.7+)
- A free Vigilmon account
Step 1: Add a Health Check Plug
LiveView's WebSocket transport sits on top of your Phoenix endpoint. A health check that validates HTTP, Ecto, and PubSub gives Vigilmon the full picture.
# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
import Plug.Conn
def init(opts), do: opts
def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
checks = %{
database: check_database(),
pubsub: check_pubsub()
}
degraded = Enum.any?(checks, fn {_, v} -> v != :ok end)
status = if degraded, do: 503, else: 200
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(%{
status: if(degraded, do: "degraded", else: "ok"),
checks: Map.new(checks, fn {k, v} -> {k, Atom.to_string(v)} end)
}))
|> 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 check_pubsub do
topic = "health:check:#{System.unique_integer([:positive])}"
Phoenix.PubSub.subscribe(MyApp.PubSub, topic)
Phoenix.PubSub.broadcast(MyApp.PubSub, topic, :ping)
result =
receive do
:ping -> :ok
after
1_000 -> :error
end
Phoenix.PubSub.unsubscribe(MyApp.PubSub, topic)
result
end
end
Register the plug in your endpoint before the router:
# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
plug MyAppWeb.Plugs.HealthCheck # before authentication middleware
plug MyAppWeb.Router
end
Test it:
curl -s http://localhost:4000/health | jq .
# {
# "status": "ok",
# "checks": { "database": "ok", "pubsub": "ok" }
# }
A PubSub failure returns 503 — surfacing the exact dependency that would cause LiveView channels to stop broadcasting.
Step 2: Configure Vigilmon HTTP Monitors
Log in to Vigilmon → Monitors → New Monitor.
Monitor 1: Health Endpoint
| Field | Value |
|---|---|
| URL | https://yourapp.com/health |
| Method | GET |
| Expected status | 200 |
| Expected body | "status":"ok" |
| Check interval | 1 minute |
Monitor 2: LiveView WebSocket Upgrade Path
LiveView negotiates a WebSocket connection at /live/websocket. An HTTP GET to this endpoint returns a 101 Switching Protocols or a redirect — but Vigilmon can confirm the path is reachable (not returning 404 or 500) which catches mis-configured Nginx upstreams and broken deploy artifacts.
| Field | Value |
|---|---|
| URL | https://yourapp.com/live/websocket |
| Method | GET |
| Expected status | 200–404 (any non-5xx) |
| Check interval | 1 minute |
Step 3: Heartbeat Monitor for GenServer Data Feeds
LiveView mounts frequently read from a GenServer or Phoenix.Tracker that polls external data. If that process crashes and the supervisor gives up retrying, your LiveViews stall silently — the process tree is healthy but data has stopped flowing.
# lib/my_app/feed_server.ex
defmodule MyApp.FeedServer do
use GenServer
require Logger
@poll_interval :timer.seconds(30)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
@impl true
def init(state) do
schedule_poll()
{:ok, state}
end
@impl true
def handle_info(:poll, state) do
case fetch_and_broadcast() do
:ok ->
ping_heartbeat()
{:error, reason} ->
Logger.error("FeedServer poll failed: #{inspect(reason)}")
# No heartbeat ping — Vigilmon alerts after the window
end
schedule_poll()
{:noreply, state}
end
defp schedule_poll, do: Process.send_after(self(), :poll, @poll_interval)
defp fetch_and_broadcast do
# Replace with your actual data-fetching logic
Phoenix.PubSub.broadcast(MyApp.PubSub, "feed:updates", {:data, %{}})
:ok
end
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:feed_heartbeat_url]
if url, do: Req.get(url, receive_timeout: 5_000)
end
end
Add config:
# config/runtime.exs
config :my_app, :vigilmon,
feed_heartbeat_url: System.get_env("VIGILMON_FEED_HEARTBEAT_URL")
In Vigilmon:
- Monitors → New Heartbeat Monitor
- Expected interval: 2 minutes (4× the 30-second poll)
- Copy the ping URL and set
VIGILMON_FEED_HEARTBEAT_URLin your environment
Step 4: Monitor LiveView Channel Recovery After Deploys
Rolling deploys disconnect existing LiveView clients. Reconnection storms can spike memory and cause the node to become unresponsive before the load balancer health check catches it.
Add a post-deploy check in your CI/CD pipeline:
#!/usr/bin/env bash
# deploy_verify.sh — run after every production deploy
set -euo pipefail
HEALTH_URL="https://yourapp.com/health"
MAX_RETRIES=12
SLEEP=10
for i in $(seq 1 $MAX_RETRIES); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL")
if [ "$STATUS" = "200" ]; then
echo "Health check passed (attempt $i)"
exit 0
fi
echo "Attempt $i: got $STATUS, retrying in ${SLEEP}s..."
sleep $SLEEP
done
echo "Health check failed after $MAX_RETRIES attempts"
exit 1
Pair this with Vigilmon's 1-minute polling: if the reconnection surge degrades your health endpoint, Vigilmon alerts independently of your deploy pipeline.
Step 5: Alert Channels
In Vigilmon → Alert Channels:
- Alert Channels → Email → add your on-call address
- Attach to all monitors
Slack
- Create a Slack Incoming Webhook for
#alerts - Alert Channels → Webhook → paste the URL
- Payload template:
{
"text": "🔴 *{{ monitor.name }}* is DOWN\n{{ monitor.url }}\nStatus: {{ event.status }}\n<https://vigilmon.online|Open Vigilmon>"
}
Vigilmon sends a matching ✅ RECOVERED notification when the monitor comes back up.
Step 6: Public Status Page
- Vigilmon → Status Pages → New Status Page
- Add your health endpoint monitor and the WebSocket path monitor
- Copy the public URL and share it in your
READMEor error pages

Summary
| Monitor | What It Catches | |---|---| | HTTP health endpoint | Database failures, PubSub outages, broken deploys | | LiveView WebSocket path | Nginx misconfiguration, endpoint process crashes | | GenServer heartbeat | Silent feed failures, stalled PubSub broadcasts |
Next Steps
- Add
:memsupmemory data to your health response to catch reconnection-storm OOM conditions early - Monitor each Fly.io region separately if you run a distributed cluster
- Use Vigilmon's response time graphs to correlate LiveView latency spikes with database query slowdowns
- Add a heartbeat for every Oban queue that drives LiveView updates
Ready to get started? Sign up for Vigilmon free — have your first monitor running in under 5 minutes.