Monitoring Node.js scheduled jobs (node-cron, BullMQ)
In-process schedulers like node-cron only run while your Node process is alive and has the schedule
registered. A crash loop, a deploy that dropped the worker, or an exception swallowed by a promise means the job
just stops, and nothing tells you. A heartbeat closes that gap: the job pings CronPup when it finishes, and you're
alerted if the ping doesn't arrive on time.
A small helper (Node 18+)
// heartbeat.js
export async function ping(url) {
try {
await fetch(url, { signal: AbortSignal.timeout(5000) });
} catch (err) {
console.warn(`heartbeat ping failed: ${err.message}`); // never let monitoring break the job
}
}
export async function monitored(url, fn) {
await ping(`${url}/start`);
try {
const result = await fn();
await ping(url);
return result;
} catch (err) {
await ping(`${url}/fail`);
throw err;
}
}
node-cron
import cron from "node-cron";
import { monitored } from "./heartbeat.js";
cron.schedule("0 3 * * *", () =>
monitored(process.env.BACKUP_PING_URL, runBackup),
{ timezone: "Europe/Berlin" });
Create a CronPup monitor with the cron expression 0 3 * * * and timezone Europe/Berlin,
so it expects the run at the same moment.
BullMQ repeatable jobs
Wrap the processor, so the ping proves a worker actually ran the job, not just that it was queued:
new Worker("reports", async (job) => {
if (job.name === "nightly-report") {
return monitored(process.env.REPORT_PING_URL, () => buildReport(job.data));
}
}, { connection });
Give the monitor a grace period that covers normal queue wait plus run time. Re-throwing keeps BullMQ's retries. A later successful attempt pings success and marks the monitor up again.
Several processes running the same schedule?
If every replica runs node-cron, each one pings. That's harmless for the heartbeat, but it usually means the job runs several times. Run the schedule in one process, or move it to a queue.
Monitor your Node.js jobs. 10 monitors are free.