Cron Jobs in Node.js: Libraries, Pitfalls & Monitoring
September 1, 2026


Running cron jobs in Node isn't the same problem as running cron jobs on a server, and treating them as interchangeable is how scheduled tasks quietly stop working. Most tutorials show you how to install node-cron and print a timestamp every minute. Almost none explain what happens when that process crashes, gets redeployed, or gets scaled to three replicas — exactly when scheduling bugs start costing real money.
This guide covers both paths for cron jobs in Node.js, compares the popular in-process libraries, and walks through the operational traps that make Node cron jobs fail without throwing a single error. Then it shows how to instrument any of them so failures actually surface.
What "Cron Jobs in Node" Actually Means
There are two fundamentally different ways to run scheduled work in a Node.js stack, and conflating them causes most of the confusion around "why isn't my cron job running."
Option one: OS-level crontab calling a Node script. The system crontab (managed by the Linux kernel's cron daemon) triggers node /app/scripts/send-invoices.js on a schedule. The script runs, exits, and the OS forgets about it until the next tick. This is simple, survives app deploys because it's decoupled from your running server process, and it's the traditional Unix approach.
Option two: an in-process scheduler library. Packages like node-cron, node-schedule, agenda, or BullMQ run inside your live Node application. There's no separate script invocation — the scheduler is a timer living in the same process as your API server, checking the clock and firing callbacks.
Node cron jobs built the second way are attractive because they live next to your business logic, share your app's config and database connections, and don't require shell access to a production box. But that convenience comes with a structural weakness this article is built around: the job only exists as long as the process does. If you're also managing traditional crontab entries alongside your Node app, crontab monitoring for catching silent cron failures is worth reading as a companion piece. The rest of this guide focuses on the in-process path, since that's where most Node.js teams increasingly build.
Scheduling Jobs with node-cron (Code Example)
node-cron is the most common starting point: zero dependencies, familiar crontab-style syntax, and — per its GitHub repository — production use across more than 220,000 repositories. Here's a node cron job example that includes the overlap protection almost every tutorial skips:
const cron = require('node-cron');
let isRunning = false;
cron.schedule('*/5 * * * *', async () => {
if (isRunning) {
console.warn('Previous run still in progress, skipping this tick');
return;
}
isRunning = true;
try {
await syncBillingRecords();
} catch (err) {
console.error('Billing sync failed:', err);
} finally {
isRunning = false;
}
});
That isRunning flag matters because node-cron won't stop a slow task from overlapping with the next tick on its own — if syncBillingRecords() takes longer than five minutes, two copies run concurrently. Newer versions of node-cron also expose built-in noOverlap and distributed-lock options, documented in the npm package, which handle this more robustly than a manual flag. For a job firing every 60 seconds — a common node cron job every minute example — this guard is non-negotiable, since even a brief slowdown compounds fast.
Other Node.js Scheduling Libraries: node-schedule, agenda, BullMQ
Node-cron is fine for lightweight, stateless, fire-and-forget work, but it's not the only option, and picking wrong causes pain later. A rough breakdown, grounded in the comparison at npm-compare:
- node-cron — crontab-style syntax, in-memory only, no persistence. Best for simple recurring tasks where losing the schedule on restart is acceptable.
- node-schedule — supports cron syntax and specific future dates/times ("run once at 2025-03-01 09:00"). The node-schedule vs node-cron choice usually comes down to whether you need one-off date scheduling, not just recurring intervals.
- agenda — persists jobs to MongoDB, so schedules survive restarts and can be inspected or rescheduled from the database. Agenda for Node.js suits teams already on Mongo who want durability without standing up a separate queue.
- BullMQ — a Redis-backed job queue with scheduling support, retries, concurrency control, and delayed jobs. Heavier to set up but built for production job processing at scale, not just timers.
- Bree — worker-thread based, good for CPU-heavy scheduled jobs that shouldn't block the event loop.
- NestJS's
@nestjs/schedule— a thin decorator layer over cron-like scheduling for teams already in the Nest ecosystem.
If your workload needs retries, persistence, or horizontal scaling guarantees, don't force node-cron to do a queue's job — see the fuller breakdown in Open Source Job Scheduler: 7 Options Compared for 2025. The node cron vs crontab decision and the "which library" decision are separate questions, and this section only answers the second one.
Why Node Cron Jobs Fail Silently
This is the part most tutorials never touch, and it's where teams get burned in production.
Deploys and crashes kill the scheduler. An in-process cron job only runs while its host process is alive. A deploy, an out-of-memory crash, or a container restart wipes the timer with it. Unlike system crontab, nothing resurrects it automatically — the next tick simply never fires, and there's no error log because there's no process left to write one.
Cluster mode and multiple instances duplicate jobs. Run your app under PM2 cluster mode or scale to three Kubernetes replicas, and each instance independently schedules the same node-cron timer. A job meant to fire once now fires three times — triggering duplicate emails, charges, or reports. This is the single most common cron job node.js production incident, and it's invisible in staging where you typically run one instance.
Async errors get swallowed. If a scheduled callback throws inside an unhandled promise rejection, many setups log it (if you're lucky) and move on. Nothing crashes, nothing alerts anyone, and the job silently stops doing its actual work on subsequent runs.
Serverless cold starts skip ticks. If your Node app runs on a serverless platform that scales to zero, an in-process scheduler has no guarantee it's even running when the tick should fire — the whole approach assumes a long-lived process that serverless explicitly avoids.
The common thread: a node cron job not running produces no exception, no crash log, and no alert. It just stops.
Adding Monitoring to a Node.js Cron Job
The fix is a heartbeat pattern: your job pings an external monitor on start, on success, and on failure. If the expected ping doesn't arrive within the expected window, the monitor — not your app — raises the alert. This is the push/heartbeat model described in Healthcheck Systems Explained: Pull, Push, and Heartbeat, and it works identically whether you're using node-cron, agenda, or BullMQ.
const PING_URL = 'https://cronevra.com/ping/your-job-id';
cron.schedule('*/5 * * * *', async () => {
await fetch(`${PING_URL}/start`);
try {
await syncBillingRecords();
await fetch(`${PING_URL}/success`);
} catch (err) {
await fetch(`${PING_URL}/fail`);
throw err;
}
});
Cronevra watches for that expected ping and fires an alert the moment it's late or missing — whether the cause was a crashed process, a deploy that never restarted the scheduler, or an exception nobody logged. That's the difference between knowing your server is "up" and knowing your scheduled task actually ran and succeeded.
Frequently Asked Questions
What's the difference between running a cron job via system crontab vs. inside a Node.js app?
System crontab is managed by the OS and survives your application's deploys and crashes because it's a separate process invoking your script on schedule. An in-process Node.js scheduler (node-cron, agenda, etc.) runs as a timer inside your live app, sharing your app's crashes, restarts, and scaling behavior — and disappears whenever the process does.
Which npm package should I use to schedule jobs in Node.js?
Use node-cron for simple, stateless recurring tasks with crontab-style syntax; node-schedule when you need one-off future date scheduling in addition to recurring jobs; agenda when you need MongoDB-backed persistence; and BullMQ when you need a Redis-backed queue with retries and concurrency control for production-scale job processing.
Why does my node-cron job silently stop running after a deploy or crash?
Because node-cron's schedule lives entirely in memory inside your running process — when that process is killed by a deploy, crash, or restart, the timer is gone with it and nothing automatically re-registers it. There's no separate daemon watching for missed runs the way system crontab has, so the failure produces no log and no alert.
How do I stop a scheduled Node.js job from running twice when I scale to multiple instances?
Each replica or PM2 cluster worker independently schedules the same in-process timer, so without coordination every instance fires it. Use a distributed lock (node-cron's built-in distributed option, a Redis lock, or a queue-based approach like BullMQ) so only one instance executes the job per tick, or move scheduling to a single dedicated worker process.
How do I get alerted when a Node.js cron job fails or doesn't run?
Add an HTTP ping at the start, success, and failure points of the job and send it to an external monitor like Cronevra. If the expected ping doesn't arrive within the scheduled window, the monitor raises the alert independently of your app, catching crashes, skipped ticks, and swallowed errors that your own logs would miss.
Add Monitoring in Five Minutes
Once a scheduled Node.js task pings on start, success, and fail, you stop finding out about broken jobs from an angry customer three days later. Cronevra turns a missing ping into an immediate alert — check the pricing page for the free tier and paste one HTTP call into your existing job today.