How to Monitor Quantum with Vigilmon
Quantum is Elixir's cron-style job scheduler. It runs jobs on a schedule using cron expressions, distributes work across cluster nodes, and integrates with OTP supervision trees. But scheduled jobs have a critical failure mode: when a job fails, hangs, or simply stops running after a node restart, there is no visible error — your schedule just silently stops executing.
This tutorial wires up reliable monitoring for Quantum-powered schedulers:
- A health endpoint that verifies the Quantum scheduler is running
- Heartbeat monitors that detect missed job executions
- HTTP uptime monitoring with Vigilmon
- Job execution tracking via telemetry
- Alerts when scheduled jobs stop firing
Why Monitor Quantum?
| Signal | What it catches | |---|---| | Scheduler process health | Quantum GenServer crash or restart loop | | Job execution rate | Jobs stopped firing after a deploy | | Job duration | Jobs that are hanging (duration exceeds cron interval) | | Cluster distribution | Jobs not running on any node in a cluster | | Missed executions | Jobs that should have run but didn't |
A standard uptime check only confirms your app is up — it won't catch a Quantum scheduler that crashed and wasn't restarted, or a job that's been silently skipping due to a runtime error.
Step 1: Add a Quantum Scheduler Health Check
Check that the Quantum scheduler process is alive and has active jobs:
# lib/my_app/health/quantum_health.ex
defmodule MyApp.Health.QuantumHealth do
@moduledoc """
Verifies the Quantum scheduler is running and has active jobs.
"""
def check do
with :ok <- check_scheduler_running(),
{:ok, jobs} <- check_active_jobs() do
{:ok, %{status: "ok", active_jobs: length(jobs)}}
else
{:error, reason} -> {:error, %{status: "error", reason: reason}}
end
end
defp check_scheduler_running do
case Process.whereis(MyApp.Scheduler) do
nil ->
{:error, "Quantum scheduler process not found"}
pid when is_pid(pid) ->
if Process.alive?(pid) do
:ok
else
{:error, "Quantum scheduler process is not alive"}
end
end
end
defp check_active_jobs do
jobs = MyApp.Scheduler.jobs()
active =
Enum.filter(jobs, fn {_name, job} ->
job.state == :active
end)
if length(active) > 0 do
{:ok, active}
else
{:error, "No active Quantum jobs found — scheduler may not have started correctly"}
end
end
end
Wire it into a controller:
# lib/my_app_web/controllers/health_controller.ex
def quantum(conn, _params) do
case MyApp.Health.QuantumHealth.check() do
{:ok, result} ->
json(conn, result)
{:error, result} ->
conn |> put_status(503) |> json(result)
end
end
Add the route:
scope "/health", MyAppWeb do
get "/quantum", HealthController, :quantum
end
Step 2: Add Vigilmon Uptime Monitoring
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - Enter your health endpoint:
https://yourapp.com/health/quantum. - Set Check interval to
1 minute. - Set Expected HTTP status to
200. - Under Advanced, add a Response body contains check:
"status":"ok". - Click Save.
Vigilmon now verifies the Quantum scheduler process is alive and has active jobs every minute.
Step 3: Add Heartbeats for Critical Jobs
The most reliable way to monitor job execution is to emit a heartbeat from within the job itself. For each critical scheduled job, add a Vigilmon heartbeat ping at the end of a successful run:
# lib/my_app/scheduler.ex
defmodule MyApp.Scheduler do
use Quantum, otp_app: :my_app
end
# config/config.exs
config :my_app, MyApp.Scheduler,
jobs: [
# Daily report job — should fire every day at 6am UTC
daily_report: [
schedule: "0 6 * * *",
task: {MyApp.Jobs.DailyReport, :run, []}
],
# Hourly sync — should fire every hour
hourly_sync: [
schedule: "@hourly",
task: {MyApp.Jobs.HourlySync, :run, []}
]
]
Add heartbeat pings to each critical job:
# lib/my_app/jobs/daily_report.ex
defmodule MyApp.Jobs.DailyReport do
require Logger
@vigilmon_heartbeat_url "https://vigilmon.online/api/heartbeat/YOUR_DAILY_REPORT_HEARTBEAT_ID"
def run do
Logger.info("[DailyReport] Starting daily report generation")
result = generate_report()
case result do
:ok ->
Logger.info("[DailyReport] Completed successfully")
ping_vigilmon()
{:error, reason} ->
Logger.error("[DailyReport] Failed: #{inspect(reason)}")
# Do NOT ping Vigilmon — the heartbeat will time out and alert
end
end
defp generate_report do
# Your report generation logic here
:ok
end
defp ping_vigilmon do
:httpc.request(:get, {String.to_charlist(@vigilmon_heartbeat_url), []}, [], [])
end
end
To create the heartbeat monitor for each job in Vigilmon:
- Click Add Monitor → Cron / Heartbeat.
- Set the expected ping interval to match the job schedule:
- Daily job:
24 hours - Hourly job:
1 hour
- Daily job:
- Set a grace period of 15–30 minutes to allow for execution time.
- Copy the generated heartbeat URL into the job module.
Step 4: Track Job Execution with Telemetry
Instrument your jobs to emit telemetry events for execution tracking:
# lib/my_app/jobs/job_instrumentation.ex
defmodule MyApp.Jobs.JobInstrumentation do
@doc """
Wraps a job function with telemetry instrumentation.
"""
def with_telemetry(job_name, fun) do
start_time = System.monotonic_time()
:telemetry.execute(
[:my_app, :quantum, :job, :start],
%{system_time: System.system_time()},
%{job: job_name}
)
result =
try do
fun.()
rescue
e ->
{:error, Exception.message(e)}
end
duration = System.monotonic_time() - start_time
:telemetry.execute(
[:my_app, :quantum, :job, :stop],
%{duration: duration},
%{job: job_name, result: result_status(result)}
)
result
end
defp result_status(:ok), do: :success
defp result_status({:ok, _}), do: :success
defp result_status({:error, _}), do: :failure
defp result_status(_), do: :unknown
end
Use it in your jobs:
defmodule MyApp.Jobs.HourlySync do
import MyApp.Jobs.JobInstrumentation
@vigilmon_heartbeat_url "https://vigilmon.online/api/heartbeat/YOUR_HOURLY_SYNC_HEARTBEAT_ID"
def run do
with_telemetry(:hourly_sync, fn ->
result = do_sync()
if result == :ok do
ping_vigilmon()
end
result
end)
end
defp do_sync do
# Sync logic here
:ok
end
defp ping_vigilmon do
:httpc.request(:get, {String.to_charlist(@vigilmon_heartbeat_url), []}, [], [])
end
end
Attach a handler to log execution metrics:
# lib/my_app/telemetry.ex
:telemetry.attach(
"quantum-job-stop",
[:my_app, :quantum, :job, :stop],
fn _event, %{duration: duration}, %{job: job, result: result}, _config ->
duration_ms = System.convert_time_unit(duration, :native, :millisecond)
require Logger
Logger.info("[Quantum] Job #{job} #{result} in #{duration_ms}ms")
end,
nil
)
Step 5: Monitor Quantum in a Clustered Environment
In a distributed Elixir cluster, Quantum can be configured to run jobs on a specific node or distribute them. Add a check that verifies the scheduler is running on the expected nodes:
# lib/my_app/health/quantum_cluster_health.ex
defmodule MyApp.Health.QuantumClusterHealth do
def check do
local_node = Node.self()
connected_nodes = Node.list()
all_nodes = [local_node | connected_nodes]
scheduler_nodes =
Enum.filter(all_nodes, fn node ->
:rpc.call(node, Process, :whereis, [MyApp.Scheduler]) != nil
end)
if length(scheduler_nodes) > 0 do
{:ok, %{
status: "ok",
scheduler_nodes: scheduler_nodes,
total_nodes: length(all_nodes)
}}
else
{:error, %{
status: "error",
reason: "Quantum scheduler not running on any cluster node",
checked_nodes: all_nodes
}}
end
end
end
Step 6: Set Up Alerts
- Go to Alert Channels → Add Channel.
- Choose Slack, Email, or PagerDuty.
- Configure separate thresholds for each monitor type:
- Scheduler process health (HTTP monitor): alert after 2 consecutive failures
- Daily report heartbeat: alert after 1 missed ping (24h job)
- Hourly sync heartbeat: alert after 1 missed ping with 30min grace period
For jobs that run billing, reporting, or data sync operations, use a dedicated PagerDuty escalation so missed executions page on-call engineers immediately.
Key Metrics to Watch
| Metric | Vigilmon feature | What to alert on |
|---|---|---|
| Scheduler process | HTTP monitor on /health/quantum | Any 5xx or process-not-found |
| Daily job execution | Heartbeat with 24h interval | Missed ping |
| Hourly job execution | Heartbeat with 1h interval + grace | Missed ping |
| Job duration | Response time chart | Duration exceeds cron interval |
| SSL certificate | Cert expiry monitor | Expires in < 14 days |
Conclusion
Quantum makes cron-style scheduling elegant in Elixir, but scheduled jobs are an invisible failure surface — when they stop, nothing in your app obviously breaks. A health endpoint that verifies the scheduler process, per-job heartbeats that only ping on successful execution, and Vigilmon watching both gives you immediate visibility when scheduled work stops happening.
Get started free at vigilmon.online.