Skip to content
Get started

How to monitor a node-cron scheduled task

node-cron schedules tasks inside your Node.js process, so its jobs only run while that process is alive — a crash, a restart, or a redeploy silently stops every scheduled task with no error. To monitor it, send a heartbeat to an external monitor from inside the scheduled task and alert when the ping doesn't arrive in the expected window. That surfaces both a task that threw and a process that died.

Why did my node-cron task stop running?

node-cron is an in-process scheduler: the schedule lives in memory inside your running Node process. That has a specific failure mode — if the process crashes, is restarted by your process manager, or is replaced during a deploy, the in-memory schedule is gone and does not resume until the process starts again and re-registers its tasks. Nothing logs that the schedule stopped.

  • The Node process crashed or was restarted (PM2, systemd, a container restart) and the schedule wasn't re-registered — or the app booted but the scheduling code path didn't run.
  • An unhandled error inside the task: an exception in the scheduled callback can stop that task from completing, and if it's uncaught it may take down the process.
  • A timezone mismatch: node-cron accepts a timezone option; without it the schedule uses the server's local time, which may not be what you expect on a UTC host.
  • Multiple instances: if you run several copies of the app (horizontal scaling), each runs its own in-process schedule, so the task fires once per instance unless you guard against it.

How do I send a heartbeat from a node-cron task?

Ping the monitor as the last action of the scheduled task, after the work succeeds. If the task throws or the process is down, no ping arrives and the monitor alerts:

scheduler.js
import cron from "node-cron";

const PING_URL = "https://ping.cronshield.com/<your-check-id>";

// "0 3 * * *" = 03:00 every day. Pin the timezone so it doesn't drift with the host.
cron.schedule(
  "0 3 * * *",
  async () => {
    try {
      await runNightlyJob();
      // Report success last. A throw above skips this and the monitor alerts.
      await fetch(PING_URL, { signal: AbortSignal.timeout(10000) });
    } catch (err) {
      console.error("nightly job failed:", err);
      await fetch(PING_URL + "?fail=1").catch(() => {});
    }
  },
  { timezone: "UTC" }
);
Because node-cron is in-process, the most valuable alert is the one you get when the whole process is down — no task runs, no ping fires, and the monitor catches it. PING_URL is a placeholder for the endpoint you get on a monitor.

Should I run scheduled jobs in-process at all?

In-process scheduling is convenient but couples the schedule to a single process's uptime. For jobs that must survive restarts and multi-instance deploys, a durable queue (Agenda, BullMQ, Bree) or an external scheduler (system cron, a platform cron) is more robust. Whichever you use, an external heartbeat is what tells you it actually ran.

Add a missed-run alert to this job

The free tier gives you a heartbeat endpoint and an email alert when an expected ping doesn't arrive. Paid tiers add the log-aware diagnosis — the last log line and a likely cause in the alert. The heartbeat receiver ships in an upcoming release; see the plans to learn what each tier adds.

Frequently asked questions

Does node-cron survive a server restart?
No. node-cron schedules live in memory in the Node process. When the process restarts, the schedule is gone until the app boots and re-registers its tasks. A run due during the downtime is not queued or caught up — which is exactly why an external heartbeat is worth adding.
Why does my node-cron job run at the wrong time?
By default node-cron uses the server's local time. On a host set to UTC (common in containers and cloud), a schedule you wrote for local time fires at a different wall-clock hour. Pass an explicit timezone option so the schedule is unambiguous.