node-cron vs node-schedule vs Croner: Task Scheduling in Node.js (2026)
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
Download Trends
| Package | Weekly Downloads | TypeScript | Timezone | Error 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
| Feature | node-cron | node-schedule | Croner |
|---|---|---|---|
| Cron syntax | ✅ | ✅ | ✅ |
| TypeScript | ✅ @types | ✅ @types | ✅ Native |
| Timezone | ✅ | ✅ | ✅ DST-aware |
| Date scheduling | ❌ | ✅ | ✅ |
| RecurrenceRule | ❌ | ✅ | ❌ |
| Error catching | ❌ Manual | ❌ Manual | ✅ catch 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 →