Cron Jobs in Node.js: A Complete node-cron Tutorial
September 5, 2026


Scheduling a cron job in Node.js looks trivial until it breaks in production — it runs fine on your laptop, then silently stops after deployment, or two overlapping executions send the same email twice. This tutorial walks through one opinionated path: install node-cron, write a real job, handle errors, stop overlaps, deploy it properly, then add monitoring so failures never go unnoticed.
What You Need Before You Start
Node.js cron job requirements are minimal: a working Node.js installation (v14 or later is fine), a task you want to run on a schedule, and basic comfort with async/await.
- Node.js installed (check with
node -v) - A code editor and terminal access
- npm or yarn for package installation
- Basic familiarity with JavaScript promises and async functions
With that in place, you can go from an empty folder to a running scheduled job in a few minutes.
Step 1: Install node-cron and Write Your First Job
node-cron is the most widely used scheduler for Node.js because it mirrors standard crontab syntax and requires no external dependencies. Install it:
npm install node-cron
A complete, runnable example that logs a message every minute:
const cron = require('node-cron');
cron.schedule('* * * * *', () => {
console.log('Running scheduled task:', new Date().toISOString());
});
require('node-cron') loads the package; cron.schedule() takes a cron expression as its first argument and a callback as its second; the callback runs every time the expression matches the current minute. Save this as index.js and run node index.js — leave it running and you'll see a new log line every 60 seconds. node-cron parses the expression, checks it against the system clock, and fires your function when it matches.
Step 2: Cron Syntax Cheat Sheet for Node.js
Node.js cron syntax follows the same five-field format as standard Unix cron: minute, hour, day-of-month, month, day-of-week. Getting one field wrong is the most common reason jobs run at the wrong time.
| Schedule | Cron Expression |
|---|---|
| Every minute | * * * * * |
| Every 5 minutes | */5 * * * * |
| Every hour, on the hour | 0 * * * * |
| Every day at midnight | 0 0 * * * |
| Weekdays at 6:00 AM | 0 6 * * 1-5 |
To double-check an expression before deploying, crontab.guru gives a visual breakdown of each field. For a schedule you'll use constantly — running something every hour — see this deeper guide on cron job syntax and monitoring for hourly schedules.
Prefer a friendlier syntax with built-in timezone support? The cron npm package offers similar functionality with a class-based API — worth a look if node-cron's function-first style doesn't fit your codebase, though node-cron remains the simpler default for most projects.
Step 3: Handle Errors and Prevent Overlapping Runs
An unhandled error inside a scheduled callback can crash the process or fail silently while leaving data in an inconsistent state. Cron job error handling starts with wrapping the task body in try/catch:
cron.schedule('0 * * * *', async () => {
try {
await sendHourlyReport();
} catch (err) {
console.error('Job failed:', err.message);
}
});
That handles errors, but not overlap. If sendHourlyReport() takes longer than the interval between runs — say a slow database query pushes a "every 5 minutes" job past the 5-minute mark — node-cron will happily start a second execution while the first is still in flight, which is how customers end up with duplicate emails or charges. Guard against overlapping jobs with a simple lock flag:
let isRunning = false;
cron.schedule('*/5 * * * *', async () => {
if (isRunning) {
console.warn('Previous run still in progress, skipping.');
return;
}
isRunning = true;
try {
await sendHourlyReport();
} catch (err) {
console.error('Job failed:', err.message);
} finally {
isRunning = false;
}
});
This pattern — check the flag, set it, run, always reset it in finally — is enough for most single-process deployments. For retries, delayed jobs, or persistence across restarts, look at Agenda or BullMQ, which back scheduling with a database or Redis instead of in-memory state.
Step 4: Run It in Production (PM2, Docker, or systemd)
node-cron only fires while the Node.js process hosting it is alive — it's a timer running inside your event loop, not a system service. Close the terminal running it in the foreground and the job stops; restart the server and the process doesn't come back unless something is configured to restart it.
To run a node cron job in production, you need a process supervisor:
- PM2:
pm2 start index.js --name cron-workerkeeps the process alive, restarts it on crash, and can start on server boot withpm2 startup. - Docker: package the app in a container with a restart policy (
restart: alwaysin Docker Compose) so the scheduler comes back up after a crash or host reboot. - systemd: for bare-metal servers, a systemd unit with
Restart=on-failuregives the same guarantee without an extra dependency.
Whichever you choose, a crashed Node process means every future scheduled run is simply skipped — no error message, no log entry, nothing. That's the gap the next step closes.
Step 5: Monitor the Job So Failures Don't Go Silent
This is the risk that catches most teams off guard: node-cron has no mechanism to tell anyone outside the process that a run failed or that the process isn't running at all. A crashed container, a bad deploy, an unhandled promise rejection — from the outside, a silently dead cron job looks identical to a healthy one with nothing to report. Node cron monitoring closes that gap by having the job check in with an external service on every successful run.
The pattern is simple — a heartbeat ping at the end of the try block:
const https = require('https');
cron.schedule('0 * * * *', async () => {
try {
await sendHourlyReport();
https.get('https://cronevra.com/ping/your-job-id');
} catch (err) {
console.error('Job failed:', err.message);
}
});
Cronevra tracks that ping and expects it on the schedule you define. Miss a check-in — because the process crashed, the deploy broke something, or the job started throwing — and Cronevra fires an alert instead of leaving you to find out when a customer complains days later. If your job is misbehaving in a way you can't explain, this diagnostic checklist for cron jobs that aren't working is a good place to start before adding monitoring.
Once your job is deployed, drop a Cronevra ping URL into the success path of the task and check Pricing to pick a plan — a missed or failed run should trigger an alert, not a support ticket.
Frequently Asked Questions
Is node-cron the best package for scheduling jobs in Node.js?
For most single-process apps, yes — node-cron is lightweight, uses familiar crontab syntax, and needs no external dependencies. If you need persistence across restarts, job retries, or distributed workers, Agenda or BullMQ are better fits since they back scheduling with a database or Redis instead of in-memory state.
Can I run a Node.js cron job without keeping a server process running 24/7?
Not with node-cron itself — it only fires while its host process is alive, so you either keep a process running via PM2, Docker, or systemd, or switch to a platform-level scheduler like a serverless cron trigger. If continuous uptime isn't feasible, an external trigger calling your endpoint on a schedule is a more reliable alternative.
Why did my node-cron job stop firing after a deployment?
Almost always because the process running it didn't survive the deploy — a redeploy killed the old process and nothing restarted the new one with the scheduler attached. Wrapping the app in PM2, a Docker restart policy, or a systemd unit with Restart=on-failure prevents this.
How do I schedule a cron job to run every 5 minutes in Node.js?
Use the expression */5 * * * * with node-cron: cron.schedule('*/5 * * * *', callback). The */5 in the minutes field means "every 5th minute," so it fires at :00, :05, :10, and so on.
What happens if my Node.js cron job throws an error mid-run?
Without a try/catch, an unhandled error can crash the process or leave the job silently incomplete with no record of what happened. Wrapping the task logic in try/catch and logging or reporting the error ensures failures are visible instead of swallowed.
How can I get alerted if my Node.js cron job fails or doesn't run?
Add a heartbeat ping to an external monitoring service, like Cronevra, at the end of each successful run so a missed check-in triggers an alert automatically. This catches both thrown errors and the harder case — a dead process that never runs the job at all — which internal logging alone can't detect.