CronCanary DocsPricingOpen app
← Integration guides

Monitor a Node.js cron job

Native fetch (Node 18+) — works with node-cron, Agenda, BullMQ repeatable jobs, or a bare setInterval.

Why Node.js scheduled jobs fail silently

node-cron, Agenda, and similar in-process schedulers all share one structural weakness: the schedule lives inside your application process. If that process crashes, gets OOM-killed, or a process manager like PM2 restarts it in a crash loop, the scheduled task simply stops firing — and there is no separate watchdog telling you, because the thing that would normally report the problem is the thing that died. A second failure mode is quieter: an async callback inside cron.schedule() that throws becomes an unhandled promise rejection. Depending on Node version and flags, that either crashes the whole process or gets logged to stderr and swallowed — in a container with no log alerting, that's the same as never happening. Timezone bugs are the third common cause: node-cron's timezone option is easy to omit, so a job scheduled for "4am" silently runs at the container's UTC 4am instead of the business's local 4am, or shifts an hour after a DST change nobody accounted for.

node-cron

Ping at both the start and the end of the scheduled callback. .catch(() => {}) on the ping calls themselves so a CronCanary outage never becomes your job's outage:

import cron from "node-cron"; const URL = "$URL"; // e.g. https://croncanary-ping.sleeezydesigns.workers.dev/<your-uuid> cron.schedule("0 * * * *", async () => { await fetch(URL + "/start").catch(() => {}); try { await doWork(); await fetch(URL).catch(() => {}); // success } catch (e) { await fetch(URL + "/fail").catch(() => {}); // failure throw e; } }, { timezone: "America/Los_Angeles" });

A small wrapper

Reuse the same pattern across every scheduled function instead of duplicating the try/catch:

async function monitored(url, fn) { await fetch(url + "/start").catch(() => {}); try { const r = await fn(); await fetch(url).catch(() => {}); return r; } catch (e) { await fetch(url + "/fail").catch(() => {}); throw e; } } await monitored("$URL", runNightlyJob);

Agenda and BullMQ repeatables

Both frameworks give you a job handler function — apply the same monitored() wrapper around the handler body (Agenda's agenda.define("name", monitored(URL, handler)), or inline inside a BullMQ Worker processor). Set the check's schedule to match the cron expression or repeat interval you registered, in the queue's configured timezone.

Catch the process dying, not just the job failing

Because the schedule lives inside the app, add a global process.on("unhandledRejection", ...) and process.on("uncaughtException", ...) handler that fires the /fail ping before the process exits or PM2 restarts it — otherwise a crash between two runs can go unreported until the next missed ping trips the grace period on its own.


Related guides


Add a live status badge to your README

Every check has a public SVG badge that shows its live status (updates within ~1 minute). Paste this into any README — it doubles as a heartbeat anyone on the team can see:

[![CronCanary](https://croncanary.fluxath.app/badge/<your-check-id>.svg)](https://croncanary.fluxath.app)

Copy the exact markdown from your check's detail page. Add ?label=your-text to customize the left label.


Ready to wire this up? Create a free check — 20 checks, all alert channels, no card.