← Blog  ·  October 2026

Node.js Cron Job Monitoring: Alerts When node-cron or Agenda Jobs Stop Running

Your nightly email digest stopped sending four days ago. node-cron is still scheduled — the process is running, no errors in the logs. You found out when a subscriber asked why they hadn't heard from you.

Node.js gives developers a rich ecosystem of scheduler libraries: node-cron, Agenda, node-schedule, cron-schedule, BullMQ. They're all excellent for getting jobs running. None of them tell you when a job stops.

This article covers the specific failure modes unique to Node.js schedulers, how to add proper error handling, and how to wire up heartbeat monitoring so you get alerted before your users notice.


Why Node.js cron failures are different

Traditional crontab failures are mostly about the scheduler not executing. Node.js scheduler failures are sneakier — the scheduler does execute, but the job itself swallows the error and exits cleanly. Three patterns cause this constantly:

Uncaught promise rejections

This is the most common silent failure mode in Node.js scheduled jobs. If your job function is async and throws inside a .then() chain without a catch, the rejection floats unhandled. In Node.js 14 and earlier this was completely silent. In newer versions it terminates the process — but with node-cron, the scheduler process dies, taking all other jobs with it:

// This silently drops the error in older Node; crashes everything in newer Node
cron.schedule('0 * * * *', async () => {
  const result = await fetchDataFromApi(); // throws on network failure
  await saveToDatabase(result);            // never reached — no alert fired
});

Process restarts losing in-memory schedules

node-cron and node-schedule store schedules in memory. When your Node process restarts — deploy, OOM kill, PM2 restart — all scheduled jobs are re-registered from your startup code. But if a deploy fails halfway, or your startup crashes after the scheduler initializes but before all jobs are registered, you end up with a subset of your jobs silently missing. No error, no log line, no alert.

Missing error handlers on Agenda and BullMQ

Agenda.js jobs emit 'fail' events that are completely silent unless you listen for them explicitly. Same with BullMQ's 'failed' queue event. Most tutorials show you how to define a job — they don't show you what happens when it throws:

// Agenda — the fail event is silently swallowed without this listener
agenda.on('fail', (err, job) => {
  console.error(`Agenda job ${job.attrs.name} failed:`, err);
  // Without this, you'll never know the job errored
});

// BullMQ — same pattern
worker.on('failed', (job, err) => {
  console.error(`BullMQ job ${job?.name} failed:`, err);
});

Adding proper error handling to node-cron

Every node-cron job should wrap its body in a try/catch and send a heartbeat ping on success. The heartbeat is what makes silent failures detectable from the outside:

import cron from 'node-cron';
import fetch from 'node-fetch';

cron.schedule('0 * * * *', async () => {
  try {
    await runMyJob();

    // Signal successful completion to your monitoring service
    await fetch('https://your-monitor/heartbeat/YOUR_TOKEN', {
      method: 'GET',
      signal: AbortSignal.timeout(5000), // don't let the ping hang the job
    });
  } catch (err) {
    console.error('Job failed:', err);
    // Optionally: notify an error tracker like Sentry here
  }
});

Key details: the heartbeat fires only on success (inside try, not finally). The ping has a 5-second timeout so a slow monitoring endpoint never blocks your job. The catch logs the error so it appears in your process logs.

Don't skip the AbortSignal.timeout() A hanging HTTP request inside a cron callback will block the Node.js event loop for that tick. If your monitoring service is slow or down, your cron job will appear to stall. Always set a short timeout on heartbeat pings.

External heartbeat monitoring — the dead man's switch pattern

The heartbeat pattern works like this: your job pings a URL at the end of each successful run. A monitoring service tracks those pings. If it doesn't receive a ping within the expected window (e.g., within 70 minutes of an hourly job), it fires an alert.

This is called a dead man's switch. It catches three failure classes with one mechanism:

You get notified for all three with a single fetch() call inside the try block.

Applying the pattern to Agenda.js

agenda.define('send-weekly-digest', async (job) => {
  await generateAndSendDigest();

  // Heartbeat — only fires on successful completion
  await fetch('https://your-monitor/heartbeat/AGENDA_DIGEST_TOKEN', {
    signal: AbortSignal.timeout(5000),
  });
});

// Always attach a fail listener — Agenda won't surface errors otherwise
agenda.on('fail', (err, job) => {
  console.error(`[Agenda] ${job.attrs.name} failed:`, err);
});

The same pattern works across the Node.js scheduler ecosystem

Whether you're using node-cron, Agenda, BullMQ, node-schedule, or cron-schedule, the heartbeat pattern is the same: run the job body, ping the monitor on success, wrap in error handling. The scheduler library doesn't matter. The monitoring service doesn't care what dispatched the job — it only cares whether the ping arrived on time.


Choosing the right monitoring window

When you register a monitor, you set a grace period: how long after the expected run time to wait before alerting. The right value depends on your job's typical runtime and how much latency is acceptable.

A few practical guidelines:

If you're not sure how long your job actually takes, check your process logs for the last 10 runs and set the grace to 1.5× the 95th-percentile duration.


Checklist: Node.js cron monitoring in 15 minutes

  1. Add try/catch to every async job body — no naked async () => { await ... } without error handling.
  2. Attach Agenda's fail event listener if you use Agenda — one line, immediately after agenda.define().
  3. Add heartbeat pings on success — one fetch() call with a 5-second timeout inside the try block.
  4. Register each monitored job with a heartbeat monitoring service and set an appropriate grace window.
  5. Test the alert — intentionally skip a ping and confirm you receive the notification.

📟 Monitor all your Node.js cron jobs

CronWatch monitors scheduled jobs across every Node.js scheduler — node-cron, Agenda.js, BullMQ, node-schedule, and cron-schedule. Add a single heartbeat ping to each job and get alerted in Slack or email within minutes of a missed window or failure.

Join the CronWatch waitlist → Parse a cron expression

← All articles  ·  CronWatch  ·  Cron parser tool