How to Monitor Exq with Vigilmon
Exq is a reliable, Redis-backed job processing library for Elixir that speaks the Sidekiq wire format. It handles retries, dead queues, and concurrency — but when Exq itself stops processing, the silence is deafening. Jobs pile up in Redis, workers go idle, and your app looks healthy until a customer reports that their order never shipped.
External monitoring fills that gap. This tutorial covers:
- A health endpoint that reports Exq queue depth and worker status
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring so you know when jobs stop running
- Slack alerts and a status page
Step 1: Add Exq to your project
If Exq isn't already installed:
# mix.exs
defp deps do
[
{:exq, "~> 0.19"},
{:redix, ">= 0.0.0"},
{:req, "~> 0.4"} # for heartbeat pings
]
end
Configure it:
# config/config.exs
config :exq,
name: Exq,
host: "127.0.0.1",
port: 6379,
namespace: "exq",
concurrency: 10,
queues: ["default", "critical", "mailers"],
poll_timeout: 50,
scheduler_enable: true,
max_retries: 25
Step 2: Build a health endpoint that includes Exq status
Add a plug that queries Exq's runtime state:
# 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 = run_checks()
status = if all_ok?(checks), do: 200, else: 503
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(checks))
|> halt()
end
def call(conn, _opts), do: conn
defp run_checks do
%{
status: status_label(exq_ok?() and db_ok?()),
exq: exq_check(),
database: db_check()
}
end
defp exq_check do
case Exq.Api.processes(Exq.Api) do
{:ok, processes} ->
queue_info = fetch_queue_info()
%{
status: "ok",
workers: length(processes),
queues: queue_info
}
{:error, reason} ->
%{status: "error", reason: inspect(reason)}
end
end
defp fetch_queue_info do
case Exq.Api.queues(Exq.Api) do
{:ok, queues} ->
Enum.map(queues, fn q ->
{:ok, size} = Exq.Api.queue_size(Exq.Api, q)
%{name: q, size: size}
end)
_ ->
[]
end
end
defp db_check do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> %{status: "ok"}
{:error, reason} -> %{status: "error", reason: inspect(reason)}
end
end
defp exq_ok? do
match?({:ok, _}, Exq.Api.processes(Exq.Api))
end
defp db_ok? do
match?({:ok, _}, Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []))
end
defp all_ok?(checks) do
checks.status == "ok"
end
defp status_label(true), do: "ok"
defp status_label(false), do: "degraded"
end
Register the plug in your endpoint before the router:
# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router
Test it:
mix phx.server
curl http://localhost:4000/health | jq .
# {
# "status": "ok",
# "exq": {
# "status": "ok",
# "workers": 3,
# "queues": [
# {"name": "default", "size": 0},
# {"name": "critical", "size": 0}
# ]
# },
# "database": {"status": "ok"}
# }
A 503 with "status": "degraded" tells you whether it's Exq or the database causing the problem — before Vigilmon even alerts you.
Step 3: Monitor the health endpoint with Vigilmon
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval: 1 minute
- Enable keyword check: body must contain
"status":"ok" - Save
The keyword check is important for Exq monitoring. An HTTP 200 with a degraded body would otherwise pass — the keyword match catches the case where Exq can't reach Redis but the web process is still up.
Step 4: Heartbeat monitoring per queue
Queue depth alone doesn't tell you whether jobs are actually being processed. A queue stuck at 100 jobs might be healthy (just busy) or dead (workers stopped). A heartbeat tells you the truth.
Add an Exq worker that pings Vigilmon at the end of every successful run:
# lib/my_app/workers/heartbeat_worker.ex
defmodule MyApp.Workers.HeartbeatWorker do
@moduledoc """
Enqueue this worker on a cron schedule to prove the queue is alive.
"""
use Exq.Worker.Base
require Logger
def perform do
url = Application.get_env(:my_app, :vigilmon)[:queue_heartbeat_url]
if url do
case Req.get(url, receive_timeout: 5_000) do
{:ok, %{status: 200}} ->
Logger.debug("Heartbeat ping sent")
{:error, reason} ->
Logger.warning("Heartbeat failed: #{inspect(reason)}")
end
end
:ok
end
end
Schedule it with Exq's built-in cron scheduler:
# config/config.exs
config :exq,
scheduler_enable: true,
scheduler_poll_timeout: 200
# In your application start or a migration, add:
# Exq.Enqueuer.enqueue_in(Exq.Enqueuer, "default", 0, MyApp.Workers.HeartbeatWorker, [])
Or use a GenServer to enqueue it on startup and re-enqueue after each run:
# lib/my_app/queue_monitor.ex
defmodule MyApp.QueueMonitor do
use GenServer
@interval :timer.minutes(5)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def init(state) do
schedule()
{:ok, state}
end
def handle_info(:check, state) do
Exq.Enqueuer.enqueue(Exq.Enqueuer, "default", MyApp.Workers.HeartbeatWorker, [])
schedule()
{:noreply, state}
end
defp schedule, do: Process.send_after(self(), :check, @interval)
end
In Vigilmon, create a Heartbeat Monitor:
- Click New Monitor → Heartbeat
- Set expected interval: 10 minutes (gives 2x buffer for a 5-minute job)
- Copy the ping URL
- Set it as
VIGILMON_QUEUE_HEARTBEAT_URLin your environment
If the heartbeat stops — because Redis is down, the worker crashed, or the scheduler stalled — Vigilmon alerts you within one missed interval.
Step 5: Monitor the dead job queue
Jobs that exhaust retries end up in Exq's dead set. You can expose this count in your health endpoint and alert when it grows too large:
# Add to your health check
defp dead_queue_check do
case Exq.Api.failed(Exq.Api) do
{:ok, jobs} ->
count = length(jobs)
status = if count < 50, do: "ok", else: "warning"
%{status: status, dead_jobs: count}
{:error, _} ->
%{status: "unknown"}
end
end
Set a keyword alert in Vigilmon to fire when the body contains "status":"warning". This catches runaway failures before they flood your dead queue and become difficult to triage.
Step 6: Slack alerts and status page
Slack alerts:
- In Vigilmon, go to Notifications → New Channel → Slack
- Paste your Slack incoming webhook URL
- Enable the channel on your Exq health monitor and your heartbeat monitor
When the job queue goes down:
🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: EU-West
2 minutes ago
When the heartbeat times out:
🔴 HEARTBEAT MISSED: Exq Queue Monitor
Expected ping within 10 minutes — none received
Last seen: 14 minutes ago
Status page:
- Go to Status Pages → New Status Page
- Add your HTTP monitor and heartbeat monitor
- Share the URL with your team
Your team can check the status page before escalating a "jobs not processing" report — saving you from ghost incidents.
What you've built
| What | How |
|------|-----|
| Health endpoint | Plug with Exq worker count + queue depth |
| Keyword check | Vigilmon keyword match on "status":"ok" |
| Uptime monitoring | Vigilmon HTTP monitor → /health |
| Queue liveness | Heartbeat worker + Vigilmon heartbeat monitor |
| Dead job alerting | Dead queue count exposed in health response |
| Slack alerts | Vigilmon Slack notification channel |
| Status page | Vigilmon public status page |
Redis and Exq keep your jobs moving. Vigilmon tells you when they stop.
Next steps
- Add per-queue heartbeat monitors if your queues have independent SLAs (e.g.
criticalmust process within 1 minute) - Expose Exq scheduled job counts in the health response to catch stuck schedulers
- Use Vigilmon's response time history to detect when Redis latency is affecting job dequeue times
Get started free at vigilmon.online.