Node.jsJavaScriptnpm11 min read

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.

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
When to use OS crontab instead:If your job is a standalone script with no shared app state (e.g., a database backup), OS crontab is fine. Use an in-process library when your job needs access to your app's database pool, config, or services.

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-in

Basic 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  # TypeScript

Cron-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 bundled

Basic 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

LibraryExpressionMeaning
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

Featurenode-cronnode-schedulecron
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 typesbuilt-in@types/node-schedulebuilt-in
Best forSimple recurring jobsMixed cron + one-timeFamiliar 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.