Skip to main content

node-cron vs node-schedule vs Croner: Task Scheduling in Node.js (2026)

·PkgPulse Team

TL;DR

node-cron is the simplest — just cron expressions, no setup, widely used. node-schedule is more flexible — supports cron syntax, Date objects, and complex recurrence rules (run every X hours from 9am to 5pm, stop on a specific date). Croner is the modern choice — TypeScript-native, handles edge cases (DST, leap years), better error handling, and actively maintained. For basic cron jobs, node-cron is fine. For production task scheduling where timezone handling and error recovery matter, use Croner.

Key Takeaways

  • node-cron: ~3M weekly downloads — simple cron syntax, minimal API, TypeScript via @types
  • node-schedule: ~1.5M weekly downloads — cron + date-based + recurrence rules
  • croner: ~600K weekly downloads — TypeScript-native, DST-aware, error recovery, best for production
  • All three run in-process — use BullMQ/Inngest for distributed job queues
  • Timezone support: Croner > node-schedule ≈ node-cron (all support TZ)
  • For production, always add error handling — cron failures often go silent

PackageWeekly DownloadsTypeScriptTimezoneError Handling
node-cron~3M✅ @types⚠️ Manual
node-schedule~1.5M✅ @types⚠️ Manual
croner~600K✅ Native✅ Excellent✅ Built-in

Cron Expression Reference

All three libraries use standard cron syntax:

┌─────────── second (optional, 0-59)
│ ┌─────── minute (0-59)
│ │ ┌───── hour (0-23)
│ │ │ ┌─── day of month (1-31)
│ │ │ │ ┌─ month (1-12)
│ │ │ │ │ ┌ day of week (0-6, 0=Sunday)
│ │ │ │ │ │
* * * * * *

Examples:
"* * * * *"        → Every minute
"0 * * * *"        → Every hour at :00
"0 9 * * 1-5"      → Mon-Fri at 9:00 AM
"0 0 1 * *"        → First day of every month
"*/5 * * * *"      → Every 5 minutes
"0 9,17 * * 1-5"   → 9 AM and 5 PM weekdays
"0 0 * * 0"        → Every Sunday midnight

node-cron

node-cron — minimal cron job scheduler for Node.js:

Basic usage

import cron from "node-cron"

// Run every minute:
cron.schedule("* * * * *", () => {
  console.log("Running every minute:", new Date().toISOString())
})

// Run at midnight every day:
cron.schedule("0 0 * * *", async () => {
  await resetDailyCounters()
})

// Run at 9 AM on weekdays (Mon-Fri):
cron.schedule("0 9 * * 1-5", async () => {
  await sendDailyDigestEmails()
})

// With second precision (non-standard but node-cron supports it):
cron.schedule("*/30 * * * * *", () => {
  // Runs every 30 seconds
  checkHealthStatus()
})

// With timezone:
cron.schedule(
  "0 9 * * *",
  async () => {
    await sendMorningReport()
  },
  {
    timezone: "America/New_York",  // Runs at 9 AM Eastern
  }
)

Start/stop control

// Create but don't start immediately:
const task = cron.schedule(
  "0 * * * *",
  async () => {
    await refreshPackageData()
  },
  { scheduled: false }
)

// Start/stop programmatically:
task.start()

// Stop during maintenance:
async function performMaintenance() {
  task.stop()
  await runMigrations()
  task.start()
}

// Destroy — removes task completely:
task.destroy()

Error handling (node-cron doesn't catch errors automatically):

import cron from "node-cron"
import { logger } from "./logger"

// Always wrap in try/catch or catch unhandled rejections:
cron.schedule("0 * * * *", async () => {
  try {
    await refreshPackageData()
    logger.info("Package data refreshed successfully")
  } catch (err) {
    logger.error({ err }, "Failed to refresh package data")
    // Error is swallowed without this catch — you'll never know it failed!
    await alertOps("Scheduled job failed: refreshPackageData")
  }
})

node-schedule

node-schedule — supports cron syntax plus Date and RecurrenceRule:

Cron syntax

import schedule from "node-schedule"

// Standard cron:
schedule.scheduleJob("0 0 * * *", async function() {
  await resetDailyStats()
})

Date-based scheduling

// Run at a specific date/time:
const launchDate = new Date("2026-04-01T09:00:00Z")
schedule.scheduleJob(launchDate, function() {
  console.log("Launch day!")
})

// Run once after a delay:
const inFiveMinutes = new Date(Date.now() + 5 * 60 * 1000)
schedule.scheduleJob(inFiveMinutes, function() {
  sendFollowUpEmail()
})

RecurrenceRule — complex schedules

import schedule from "node-schedule"

// RecurrenceRule for fine-grained control:
const rule = new schedule.RecurrenceRule()
rule.dayOfWeek = [1, 2, 3, 4, 5]   // Monday-Friday
rule.hour = 9                         // 9 AM
rule.minute = 0
rule.tz = "America/New_York"

schedule.scheduleJob(rule, async function() {
  await sendDailyReport()
})

// Run every 30 minutes between 8 AM and 6 PM:
const halfHourly = new schedule.RecurrenceRule()
halfHourly.minute = [0, 30]
halfHourly.hour = new schedule.Range(8, 18)  // 8 AM - 6 PM

schedule.scheduleJob(halfHourly, function() {
  checkApiHealth()
})

Managing scheduled jobs

const job = schedule.scheduleJob("0 * * * *", async function() {
  await syncData()
})

// Cancel a specific job:
job.cancel()

// Reschedule:
job.reschedule("*/30 * * * *")  // Now runs every 30 minutes

// Check next invocation:
console.log("Next run:", job.nextInvocation())

Croner

Croner — production-grade cron with TypeScript-native types and robust error handling:

Basic usage

import { Cron } from "croner"

// Schedule with automatic error handling:
const job = Cron("0 * * * *", { catch: true }, async () => {
  await refreshPackageData()
})
// catch: true — catches errors and continues running (won't kill the cron job)

// With timezone (DST-aware):
const job2 = Cron(
  "0 9 * * *",
  { timezone: "America/New_York" },
  async () => {
    await sendDailyReport()
  }
)
// Croner correctly handles DST — 9 AM stays 9 AM even when clocks change

Error handling and retry

import { Cron } from "croner"
import { logger } from "./logger"

const job = Cron(
  "0 * * * *",
  {
    timezone: "UTC",
    catch: (err, job) => {
      // Custom error handler:
      logger.error({ err, jobName: job.name }, "Cron job failed")
      notifySlack(`Cron failed: ${err.message}`)
    },
    name: "hourly-sync",  // Named jobs for identification
  },
  async () => {
    await syncPackageData()
  }
)

Start/stop and status

const job = Cron("* * * * *", { paused: true }, () => {
  console.log("tick")
})

job.resume()  // Start running

// Check status:
console.log(job.isRunning())   // Currently executing
console.log(job.isStopped())   // Has been stopped
console.log(job.nextRun())     // Next run time (Date object)
console.log(job.currentRun())  // Currently running (or null)
console.log(job.previousRun()) // Last run time (Date object)

// Stop after N runs:
let count = 0
const limitedJob = Cron("* * * * *", () => {
  count++
  if (count >= 5) limitedJob.stop()  // Stop after 5 runs
})

Multiple schedules for the same handler

import { Cron } from "croner"

// Run at different schedules but same handler:
const handler = async () => {
  await checkApiHealth()
}

Cron("*/5 9-17 * * 1-5", handler)   // Every 5 min, 9-5 on weekdays
Cron("*/15 * * * 6,0", handler)     // Every 15 min on weekends

Feature Comparison

Featurenode-cronnode-scheduleCroner
Cron syntax
TypeScript✅ @types✅ @types✅ Native
Timezone✅ DST-aware
Date scheduling
RecurrenceRule
Error catching❌ Manual❌ Manualcatch option
Named jobs
Pause/resume
Next run query
Second precision
Active maintenance

When to Use Each

Choose node-cron if:

  • Simple scripts with standard cron schedules
  • You're already familiar with it and it works
  • Minimal setup is priority — just import and schedule

Choose node-schedule if:

  • You need to schedule jobs at specific dates (not just recurring)
  • Complex recurrence rules (every 30 min between 8-6 on weekdays)
  • You need the "run once on this specific date" pattern

Choose Croner if:

  • Production applications where silent failures are a problem
  • DST-sensitive schedules (schedules at 9 AM must always run at 9 AM, not 8 or 10)
  • You want built-in error handling with custom error callbacks
  • TypeScript-first development — Croner's types are native, not @types additions

Consider BullMQ or Inngest if:

  • Jobs need to survive server restarts (stored in Redis/Inngest)
  • Distributed task execution across multiple servers
  • Job history, retry logic, and observability are requirements

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on node-cron v3.x, node-schedule v2.x, and Croner v8.x.

Compare task scheduling and automation packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.