How to Monitor Cron Jobs in Node.js
node-cron runs inside your process. If the process dies, so does every schedule, and nothing reports it.
Monitoring scheduled work in Node.js depends on a choice most projects make without thinking about it: whether the scheduler runs inside your application process or outside it.
node-cron, node-schedule and Agenda all run in-process. That has a consequence worth being explicit about.
In-process schedulers share the fate of the process
If your application crashes, is OOM-killed, or is restarted by a deploy that fails halfway, the schedules go with it. There is no separate daemon holding them.
Worse, a process that is alive but has an event loop blocked by synchronous work will not fire timers either. The health endpoint may still answer because the request arrived before the block, and the process looks fine from outside while every schedule is frozen.
An external heartbeat is the only thing that notices, because it is the only participant that is not inside the process that stopped working.
Set the timezone explicitly. node-cron defaults to the system timezone, which is UTC in most containers and local time on a developer machine, and that difference produces schedules that work in testing and fire at the wrong hour in production.
Unhandled rejections in async callbacks
This is the Node-specific trap. If your scheduled callback is async and rejects without a catch, node-cron cannot await it and the rejection goes unhandled.
Depending on your Node version and flags, that either prints a warning or terminates the process. Neither outcome tells you which job failed, and the terminating case takes every other schedule down with it.
Always wrap the body of an async scheduled callback in try/catch, as above. Relying on a global unhandledRejection handler works, but it leaves you with an alert that does not say which job caused it.
PM2 restarts hide the problem
PM2 restarting a crashed process is useful and it removes the evidence. The application comes back, the schedules re-register, and unless you look at pm2 list you will not know it happened.
A restart count that grows steadily means jobs are being interrupted mid-run on a regular basis. A heartbeat with a tight interval surfaces this as missed runs rather than as a number nobody checks.
PM2 in cluster mode adds a second issue: every instance registers the same schedules, so a four-instance cluster runs each job four times. Either run the scheduler in a single dedicated process or gate it on process.env.NODE_APP_INSTANCE === '0'.
When to use system cron instead
| In-process (node-cron) | System cron | |
|---|---|---|
| Survives app crash | No | Yes |
| Survives a blocked event loop | No | Yes |
| Shares app code and config | Yes | Needs a separate entry point |
| Duplicate runs in a cluster | Likely | No |
| Startup cost per run | None | A full Node boot |
For anything that must not be missed, such as billing or reconciliation, system cron calling node script.js is the more robust arrangement despite the startup cost. Keep node-cron for lightweight work where a missed run is recoverable.
Related guides
The system cron approach is covered in what is a cron job. Go has a similar in-process pattern, in monitoring cron jobs in Go.