tutorial

How to Monitor Quantum with Vigilmon

Quantum brings cron-style job scheduling to Elixir — but scheduled jobs fail silently when nodes crash or clocks drift. Here's how to monitor Quantum schedulers end-to-end with Vigilmon.

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

  1. Log in to vigilmon.online and click Add Monitor.
  2. Set Type to HTTP / HTTPS.
  3. Enter your health endpoint: https://yourapp.com/health/quantum.
  4. Set Check interval to 1 minute.
  5. Set Expected HTTP status to 200.
  6. Under Advanced, add a Response body contains check: "status":"ok".
  7. 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:

  1. Click Add MonitorCron / Heartbeat.
  2. Set the expected ping interval to match the job schedule:
    • Daily job: 24 hours
    • Hourly job: 1 hour
  3. Set a grace period of 15–30 minutes to allow for execution time.
  4. 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

  1. Go to Alert ChannelsAdd Channel.
  2. Choose Slack, Email, or PagerDuty.
  3. 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.

Monitor your app with Vigilmon

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

Start free →