tutorial

How to Monitor Exq with Vigilmon

Add uptime monitoring, heartbeat checks, and alerts to your Exq background job queue in Elixir — so silent job failures and queue stalls don't go undetected.

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

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute
  5. Enable keyword check: body must contain "status":"ok"
  6. 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:

  1. Click New Monitor → Heartbeat
  2. Set expected interval: 10 minutes (gives 2x buffer for a 5-minute job)
  3. Copy the ping URL
  4. Set it as VIGILMON_QUEUE_HEARTBEAT_URL in 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:

  1. In Vigilmon, go to Notifications → New Channel → Slack
  2. Paste your Slack incoming webhook URL
  3. 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:

  1. Go to Status Pages → New Status Page
  2. Add your HTTP monitor and heartbeat monitor
  3. 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. critical must 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.

Monitor your app with Vigilmon

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

Start free →