How to Monitor Bandit with Vigilmon
Bandit is a pure Elixir HTTP server implementing HTTP/1.1, HTTP/2, and WebSocket protocols from scratch. Designed as a modern, Plug-compatible alternative to Cowboy, Bandit offers superior observability through telemetry events, improved HTTP/2 performance, and a codebase written entirely in Elixir for easier debugging and introspection.
Phoenix 1.7.11+ defaults to Bandit as its HTTP adapter. If you've upgraded Phoenix recently, your app is likely already running on Bandit — and its rich telemetry integration makes it an excellent candidate for deep monitoring.
This tutorial adds production observability to a Bandit-backed application:
- A health check plug with Bandit-specific metrics
- HTTP uptime monitoring with Vigilmon
- Telemetry-driven connection and request tracking
- Alerting via Slack
- Heartbeat monitoring for background jobs
Step 1: Add Bandit to your project
If you're not already using Bandit, add it to your mix.exs:
# mix.exs
defp deps do
[
{:bandit, "~> 1.0"},
{:plug, "~> 1.14"},
{:jason, "~> 1.4"},
{:req, "~> 0.4"}
]
end
For a Phoenix app, configure Bandit as the adapter in config/config.exs:
# config/config.exs
config :my_app, MyAppWeb.Endpoint,
adapter: Bandit.PhoenixAdapter
Step 2: Add a health check plug
# 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(),
memory: check_memory(),
bandit: check_bandit()
}
status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(%{
status: (if status == 200, do: "ok", else: "degraded"),
checks: checks,
server: "bandit"
}))
|> 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
rescue
_ -> :error
end
defp check_bandit do
# Bandit registers itself under ThousandIsland — verify it's alive
case Process.whereis(MyAppWeb.Endpoint.HTTP) do
nil -> :error
pid when is_pid(pid) ->
if Process.alive?(pid), do: :ok, else: :error
end
rescue
_ -> :ok # process name not registered in all configurations
end
defp check_memory do
case :memsup.get_system_memory_data() do
[] -> :ok
data ->
total = Keyword.get(data, :total_memory, 1)
free = Keyword.get(data, :free_memory, total)
used_pct = (total - free) / total * 100
if used_pct < 90, do: :ok, else: :error
end
end
end
Mount it in your Phoenix 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 other plugs
plug MyAppWeb.Router
end
Test it:
mix phx.server
curl http://localhost:4000/health
# {"status":"ok","checks":{"database":"ok","memory":"ok","bandit":"ok"},"server":"bandit"}
Step 3: Instrument with Bandit's telemetry
Bandit emits rich :telemetry events for every connection and request lifecycle. Attach handlers to track request throughput, error rates, and connection counts:
# lib/my_app/bandit_telemetry.ex
defmodule MyApp.BanditTelemetry do
require Logger
def attach do
:telemetry.attach_many(
"bandit-metrics",
[
[:bandit, :request, :stop],
[:bandit, :request, :exception],
[:bandit, :websocket, :stop]
],
&handle_event/4,
nil
)
end
def handle_event([:bandit, :request, :stop], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.info("Bandit request completed",
method: metadata.conn.method,
path: metadata.conn.request_path,
status: metadata.conn.status,
duration_ms: duration_ms,
http_version: metadata.conn.http_protocol
)
# Emit to your metrics backend (StatsD, Prometheus, etc.)
# :telemetry.execute([:my_app, :http, :request], %{duration: duration_ms}, metadata)
end
def handle_event([:bandit, :request, :exception], _measurements, metadata, _config) do
Logger.error("Bandit request exception",
path: metadata.conn.request_path,
error: inspect(metadata.error)
)
end
def handle_event([:bandit, :websocket, :stop], measurements, metadata, _config) do
duration_s = System.convert_time_unit(measurements.duration, :native, :second)
Logger.info("WebSocket connection closed",
path: metadata.conn.request_path,
duration_s: duration_s
)
end
end
Start the telemetry handler in your application:
# lib/my_app/application.ex
def start(_type, _args) do
MyApp.BanditTelemetry.attach()
children = [
MyApp.Repo,
MyAppWeb.Endpoint
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
Step 4: Set up HTTP monitoring in Vigilmon
Point Vigilmon at your health endpoint:
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval: 1 minute (paid) or 5 minutes (free)
- Under Advanced, add a keyword check for
"bandit"to verify the correct server is responding - Save
Why external monitoring matters with Bandit:
Bandit's HTTP/2 implementation multiplexes requests over a single connection. Internal health checks run over existing connections and may miss scenarios where:
- New HTTP/2 connections fail to establish (TLS ALPN negotiation issues)
- HTTP/1.1 fallback is broken for clients that don't support HTTP/2
- WebSocket upgrades fail after a code change
Vigilmon's external probe establishes a fresh connection on every check, catching connection-establishment failures your internal checks miss.
Step 5: Monitor HTTP/2 upgrade path
If your deployment supports HTTP/2 (via a reverse proxy or Bandit's native TLS), add a separate monitor targeting the HTTPS endpoint:
In Vigilmon:
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Enable SSL certificate monitoring → Vigilmon alerts 30 days before expiry
- Set response time threshold to 1000 ms (HTTP/2 should be fast)
Step 6: Alerts via Slack
In Vigilmon, go to Notifications → New Channel → Slack and paste your Slack incoming webhook URL.
Enable the channel on your monitor. Down alerts look like:
🔴 DOWN: yourdomain.com/health
Status: 503 Service Unavailable
Detected from: EU-West, US-East
2 minutes ago
Recovery:
✅ RECOVERED: yourdomain.com/health
Downtime: 4 minutes
Set a 2-minute failure grace period in Vigilmon before alerting. This prevents noise from brief deploy windows when Bandit restarts the listener.
Step 7: Heartbeat monitoring for Oban jobs
If your Phoenix/Bandit app runs Oban for background processing:
# lib/my_app/workers/report_worker.ex
defmodule MyApp.Workers.ReportWorker do
use Oban.Worker, queue: :reports, max_attempts: 3
require Logger
@impl Oban.Worker
def perform(%Oban.Job{args: args}) do
with :ok <- generate_report(args),
:ok <- deliver_report(args) do
ping_heartbeat()
:ok
else
{:error, reason} = error ->
Logger.error("ReportWorker failed: #{inspect(reason)}")
error
end
end
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:report_heartbeat_url]
if url, do: Req.get(url, receive_timeout: 5_000)
end
defp generate_report(_args), do: :ok
defp deliver_report(_args), do: :ok
end
Add config:
# config/runtime.exs
config :my_app, :vigilmon,
report_heartbeat_url: System.get_env("VIGILMON_REPORT_HEARTBEAT_URL")
In Vigilmon, create a Heartbeat Monitor:
- New Monitor → Heartbeat
- Set expected interval to match your Oban schedule (e.g. 25 hours for a daily job)
- Copy the ping URL → set as
VIGILMON_REPORT_HEARTBEAT_URL
Step 8: Status page
- Status Pages → New Status Page in Vigilmon
- Add all monitors (HTTP, heartbeats)
- Copy the public URL
README badge:

What you've built
| What | How |
|------|-----|
| Health check plug | HealthCheck plug with Bandit process check |
| Telemetry instrumentation | [:bandit, :request, :stop] event handler |
| HTTP uptime monitoring | Vigilmon HTTP monitor → /health |
| HTTP/2 connection monitoring | External Vigilmon probe on fresh connections |
| SSL certificate alerts | Vigilmon HTTPS monitor with expiry warnings |
| Slack downtime alerts | Vigilmon Slack notification channel |
| Oban job monitoring | Heartbeat ping on perform/1 success |
| Status page | Vigilmon public status page |
Bandit handles the connections. Vigilmon verifies they keep working from the outside.
Next steps
- Attach to
[:bandit, :request, :stop]events and emit p95/p99 latency metrics to your observability stack - Use Vigilmon's response time history to track latency regressions after HTTP/2 changes
- Add separate heartbeat monitors for each Oban queue that processes business-critical work
- Test your HTTP/2 health check with
curl --http2 https://yourdomain.com/healthto verify the upgrade path works
Get started free at vigilmon.online.