Node.js Cron Job Guide: node-cron, node-schedule, and cron
Node.js has no built-in cron. You need a library. This guide covers the three most popular npm packages — node-cron, node-schedule, and cron — with syntax, timezone support, lifecycle management, and production best practices.
Contents
Why Node.js Needs a Cron Library
The OS-level crontabruns shell commands — it doesn't know about your Node.js app's dependencies, environment variables, database connections, or in-memory state. Running a separate Node process per cron job adds startup overhead and loses shared resources.
In-process Node.js cron libraries solve this by running scheduled tasks inside your running application:
- • No startup overhead — reuse DB connections, caches, service objects
- • Full access to app environment and configuration
- • Easier logging, error handling, and observability
- • Portable — works the same on Linux, macOS, Windows, Docker, and cloud
node-cron: The Most Popular Choice
node-cron (~5M weekly downloads) is the go-to choice for most Node.js projects. It uses a 5-field cron syntax (with optional seconds as a 6th field) and has a clean, minimal API.
Installation
npm install node-cron
# TypeScript — types are built-inBasic Usage
import cron from 'node-cron';
// Run every minute
cron.schedule('* * * * *', () => {
console.log('Running every minute');
});
// Run at 2:30 AM every day
cron.schedule('30 2 * * *', async () => {
await sendDailyReport();
});
// Run at 9 AM Monday–Friday
cron.schedule('0 9 * * 1-5', async () => {
await sendStandupReminder();
});
// Run every 5 minutes
cron.schedule('*/5 * * * *', () => {
checkHealthEndpoints();
});6-Field Syntax (with Seconds)
// Enable 6-field mode with { scheduled: true }
// Fields: second minute hour day month weekday
cron.schedule('*/30 * * * * *', () => {
// Runs every 30 seconds
pollExternalApi();
});Timezone Support
// Schedule in a specific timezone
cron.schedule(
'0 9 * * 1-5', // 9 AM weekdays
async () => {
await sendMorningDigest();
},
{
timezone: 'America/New_York',
}
);Start / Stop / Destroy
// Create but don't start immediately
const task = cron.schedule(
'0 * * * *',
() => { syncDatabase(); },
{ scheduled: false } // won't run until started
);
task.start(); // begin executing
task.stop(); // pause (can restart)
task.destroy(); // remove from scheduler (cannot restart)Validate Before Scheduling
import cron from 'node-cron';
const expression = process.env.SYNC_SCHEDULE ?? '0 */6 * * *';
if (!cron.validate(expression)) {
throw new Error(`Invalid cron expression: ${expression}`);
}
cron.schedule(expression, syncUsers);node-schedule: Cron + One-Time Jobs
node-schedule supports both cron-style recurring schedules and one-time future jobs. It exposes a RecurrenceRule API that is more explicit than cron strings, useful when you want to avoid memorizing syntax.
Installation
npm install node-schedule
npm install --save-dev @types/node-schedule # TypeScriptCron-String Scheduling
import schedule from 'node-schedule';
// Standard 5-field cron expression
const job = schedule.scheduleJob('0 3 * * *', async () => {
await runNightlyBackup();
});
// Cancel when done
job.cancel();RecurrenceRule API
import schedule, { RecurrenceRule } from 'node-schedule';
const rule = new RecurrenceRule();
rule.dayOfWeek = [1, 2, 3, 4, 5]; // Monday–Friday
rule.hour = 9;
rule.minute = 0;
rule.tz = 'America/Chicago';
const job = schedule.scheduleJob(rule, () => {
sendWeeklyDigest();
});One-Time Future Jobs
import schedule from 'node-schedule';
// Schedule a single future event
const launchDate = new Date(2026, 11, 31, 0, 0, 0); // Dec 31 2026 midnight
const job = schedule.scheduleJob(launchDate, () => {
triggerNewYearCampaign();
});
// Reschedule to a different time
job.reschedule('0 0 1 1 *'); // Every Jan 1
// job.nextInvocation() returns the next Date
console.log('Next run:', job.nextInvocation());cron: The Original Node.js CronJob
The cron package (originally cron.js) uses a 6-field format where the first field is seconds— matching the convention used by Java Quartz and Spring @Scheduled. If you're migrating from those ecosystems, this mapping will feel natural.
Installation
npm install cron
# TypeScript types are bundledBasic Usage
import { CronJob } from 'cron';
// 6-field: seconds minutes hours day month weekday
const job = new CronJob(
'0 30 2 * * *', // every day at 02:30:00
async () => {
await runDailyReport();
},
null, // onComplete (optional)
true, // start immediately
'America/New_York' // timezone
);
// Control lifecycle
job.stop();
job.start();
// Check next execution
console.log('Next:', job.nextDate().toISO());Key Difference: Seconds-First Field Order
| Library | Expression | Meaning |
|---|---|---|
| node-cron (5-field) | 30 2 * * * | 02:30 every day |
| cron (6-field) | 0 30 2 * * * | 02:30:00 every day |
| cron (6-field) | */10 * * * * * | Every 10 seconds |
Comparison Table
| Feature | node-cron | node-schedule | cron |
|---|---|---|---|
| npm weekly downloads | ~5M | ~2M | ~1M |
| Cron expression syntax | ✓ (5 or 6 fields) | ✓ + Rule API | ✓ (5 or 6 fields) |
| Seconds field | ✓ 6th field opt. | ✓ in Rule | ✓ 1st field |
| Timezone support | ✓ option | ✓ option | ✓ option |
| Start/stop task | ✓ task.start/stop | ✓ job.cancel() | ✓ job.start/stop |
| Async handlers | ✓ native | ✓ native | ✓ native |
| One-time jobs | ✗ | ✓ scheduleJob(date) | ✗ |
| TypeScript types | built-in | @types/node-schedule | built-in |
| Best for | Simple recurring jobs | Mixed cron + one-time | Familiar cron semantics |
Production Best Practices
1. Prevent Overlapping Executions
If a job takes longer than its interval, you'll get concurrent runs. Use a flag or a distributed lock:
let running = false;
cron.schedule('*/1 * * * *', async () => {
if (running) {
console.warn('Previous run still in progress — skipping');
return;
}
running = true;
try {
await heavyJob();
} finally {
running = false;
}
});2. Always Handle Errors
Unhandled promise rejections in cron callbacks crash the process or go silently unnoticed:
cron.schedule('0 2 * * *', async () => {
try {
await runNightlyBackup();
logger.info('Backup completed');
} catch (err) {
logger.error({ err }, 'Backup failed');
await alertOncall('Backup job failed', err);
}
});3. Use UTC or Explicit Timezones
All three libraries default to the server's local timezone. Always pass an explicit timezone option, or schedule in UTC to avoid DST surprises. Use IANA names like America/New_York, not offsets like -05:00.
4. Stop Jobs on Graceful Shutdown
const tasks: cron.ScheduledTask[] = [];
tasks.push(cron.schedule('0 * * * *', hourlySync));
tasks.push(cron.schedule('0 3 * * *', dailyBackup));
// Graceful shutdown
process.on('SIGTERM', () => {
tasks.forEach(t => t.destroy());
process.exit(0);
});5. Externalize Cron Expressions
Store cron expressions in environment variables or config to change schedules without code deploys:
// .env
SYNC_SCHEDULE="0 */6 * * *"
REPORT_SCHEDULE="0 8 * * 1"
// app.ts
const syncSchedule = process.env.SYNC_SCHEDULE!;
if (!cron.validate(syncSchedule)) {
throw new Error(`Invalid SYNC_SCHEDULE: ${syncSchedule}`);
}
cron.schedule(syncSchedule, syncUsers);6. Use a Monitoring Service in Production
Cron jobs fail silently. Ping a dead-man's-switch service after each successful run so you get alerted if a job stops running on schedule. See our Cron Job Monitoring Guide for setup instructions.
Which Library Should You Choose?
- →node-cron: Most downloads, minimal API, great TypeScript support. Best default choice for recurring jobs.
- →node-schedule: Choose this when you also need one-time future jobs, or prefer the RecurrenceRule object API over cron strings.
- →cron: Choose this if your team comes from a Java/Spring background and prefers the seconds-first 6-field format.