Skip to main content

node-cron vs node-schedule vs Agenda: Job Scheduling in Node.js (2026)

·PkgPulse Team

TL;DR

node-cron is the simplest cron scheduler — pure in-memory, cron syntax, no database needed, perfect for simple recurring tasks in a single-process app. node-schedule is more flexible — supports both cron and date/recurrence rule syntax, and can schedule one-off jobs at specific times. Agenda is the most production-ready — persists jobs in MongoDB, handles distributed systems (multiple server instances), and retries failed jobs. In 2026: node-cron for simple cron tasks, Agenda (or BullMQ) for persistent, reliable job scheduling in production.

Key Takeaways

  • node-cron: ~3M weekly downloads — in-memory, cron syntax, no persistence, timezone support
  • node-schedule: ~2M weekly downloads — cron + date rules, flexible scheduling, no persistence
  • agenda: ~300K weekly downloads — MongoDB-backed persistence, distributed, retries, job management UI
  • In-memory schedulers (node-cron, node-schedule) lose jobs on process restart — use for idempotent tasks
  • Agenda stores jobs in MongoDB — survived restarts, tracks history, supports job priorities
  • For Redis-backed scheduling, see BullMQ (covered separately)

Cron Syntax Quick Reference

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

Examples:
  "0 * * * *"    — every hour at :00
  "*/15 * * * *" — every 15 minutes
  "0 9 * * 1-5"  — 9am Monday-Friday
  "0 0 1 * *"    — midnight on the 1st of each month
  "0 0 * * 0"    — midnight every Sunday
  "*/5 9-17 * * 1-5" — every 5min, 9am-5pm, Mon-Fri

node-cron

node-cron — cron scheduler for Node.js:

Basic scheduling

import cron from "node-cron"

// Schedule a task with cron syntax:
cron.schedule("0 * * * *", () => {
  // Runs every hour at :00
  console.log("Hourly task running at", new Date().toISOString())
  syncPackageDownloads()
})

// Every 15 minutes:
cron.schedule("*/15 * * * *", async () => {
  await refreshHealthScores()
})

// Every day at 2am:
cron.schedule("0 2 * * *", async () => {
  await generateDailyReport()
  await cleanupOldLogs()
})

// Weekdays at 9am:
cron.schedule("0 9 * * 1-5", () => {
  sendWeeklyDigest()
})

Timezone support

import cron from "node-cron"

// Schedule in a specific timezone:
cron.schedule(
  "0 9 * * 1-5",
  () => {
    sendMorningDigest()
  },
  {
    timezone: "America/New_York",  // Runs at 9am ET, not server time
  }
)

// Schedule at midnight UTC regardless of server timezone:
cron.schedule(
  "0 0 * * *",
  () => {
    runMidnightJob()
  },
  {
    timezone: "UTC",
  }
)

Task control

import cron from "node-cron"

// Get a reference to the task:
const task = cron.schedule("*/5 * * * *", () => {
  processQueue()
})

// Stop the task:
task.stop()

// Start (or restart) the task:
task.start()

// Destroy (can't restart after this):
task.destroy()

// Validate a cron expression:
const isValid = cron.validate("*/5 * * * *")  // true
const invalid = cron.validate("*/5 * * * * *")  // false (node-cron is 5-field only)

Prevent overlap

import cron from "node-cron"

let isRunning = false

cron.schedule("*/5 * * * *", async () => {
  // Prevent overlapping execution if task takes longer than interval:
  if (isRunning) {
    console.log("Previous run still in progress, skipping")
    return
  }

  isRunning = true
  try {
    await longRunningTask()
  } catch (err) {
    console.error("Task failed:", err)
  } finally {
    isRunning = false
  }
})

node-schedule

node-schedule — flexible scheduling with cron and recurrence rules:

Cron-style scheduling

import schedule from "node-schedule"

// Cron syntax (same as node-cron):
const job = schedule.scheduleJob("0 0 * * *", () => {
  runMidnightJob()
})

// Cancel:
job.cancel()

// Reschedule:
job.reschedule("0 6 * * *")

Recurrence rules (non-cron)

import schedule from "node-schedule"

// Recurrence Rule — more readable than cron strings:
const rule = new schedule.RecurrenceRule()
rule.dayOfWeek = [new schedule.Range(0, 6)]  // Every day
rule.hour = 14   // At 2pm
rule.minute = 30 // At :30

const job = schedule.scheduleJob(rule, () => {
  sendAfternoonUpdate()
})

// Day 1 of every month at 3am:
const monthlyRule = new schedule.RecurrenceRule()
monthlyRule.date = 1    // 1st of month
monthlyRule.hour = 3
monthlyRule.minute = 0
monthlyRule.tz = "America/Chicago"

schedule.scheduleJob(monthlyRule, () => {
  generateMonthlyReport()
})

One-off scheduling (specific date/time)

import schedule from "node-schedule"

// Schedule a job at a specific future time:
const fireDate = new Date(2026, 2, 15, 9, 0, 0)  // March 15, 2026 at 9am

const job = schedule.scheduleJob(fireDate, () => {
  sendLaunchAnnouncement()
})
// Fires once at the exact time, then the job is done

// Schedule relative to now:
const in30Minutes = new Date(Date.now() + 30 * 60 * 1000)
const reminder = schedule.scheduleJob(in30Minutes, () => {
  sendReminder(userId)
})

// Cancel before it fires:
reminder.cancel()

Multiple named jobs

import schedule from "node-schedule"

// Named jobs for management:
schedule.scheduleJob("hourly-sync", "0 * * * *", syncData)
schedule.scheduleJob("daily-cleanup", "0 3 * * *", cleanup)
schedule.scheduleJob("weekly-report", "0 9 * * 1", weeklyReport)

// Access by name:
const job = schedule.scheduledJobs["hourly-sync"]
console.log("Next invocation:", job.nextInvocation())

// Cancel all:
schedule.gracefulShutdown()

Agenda

Agenda — MongoDB-backed job scheduler:

Setup

import Agenda from "agenda"

const agenda = new Agenda({
  db: {
    address: process.env.MONGODB_URI!,
    collection: "agendaJobs",
  },
  processEvery: "30 seconds",     // How often to check for jobs to run
  defaultLockLifetime: 10 * 60 * 1000,  // 10 minute lock (for long jobs)
})

// Define job handlers:
agenda.define("sync package downloads", async (job) => {
  const { packageName } = job.attrs.data
  await syncDownloadsForPackage(packageName)
})

agenda.define("send weekly digest", async (job) => {
  const { userId, email } = job.attrs.data
  await sendDigestEmail(userId, email)
})

agenda.define("cleanup old data", async (job) => {
  const deleted = await db.auditLog.deleteMany({
    createdAt: { $lt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) },
  })
  job.attrs.result = { deleted: deleted.count }
})

// Start processing:
await agenda.start()

Recurring jobs

// Schedule recurring jobs:
await agenda.every("1 hour", "sync package downloads", { packageName: "react" })
await agenda.every("1 day at 2:00am", "cleanup old data")

// Cron syntax also works:
await agenda.every("0 9 * * 1-5", "send weekly digest", { userId: "all", email: "digest@example.com" })

// Remove existing schedule and create fresh:
await agenda.every("5 minutes", "sync package downloads", {}, {
  skipImmediate: true,  // Don't run immediately on startup
})

One-off jobs

// Schedule a one-off job:
await agenda.schedule("in 30 minutes", "send welcome email", {
  userId: "user_123",
  email: "user@example.com",
})

await agenda.schedule("next Monday at 9am", "send weekly digest", {
  userId: "user_123",
})

// Run now:
await agenda.now("sync package downloads", { packageName: "vue" })

Job concurrency and locking

// Control how many concurrent instances of a job run:
agenda.define("import packages", { concurrency: 2 }, async (job) => {
  // Max 2 instances running at once across all server instances
  const { batchId } = job.attrs.data
  await importPackageBatch(batchId)
})

// Priority — higher numbers run first:
agenda.define("critical-sync", { priority: "high" }, async (job) => {
  await criticalSync()
})
// Priority values: "highest" (20), "high" (10), "normal" (0), "low" (-10), "lowest" (-20)

Error handling and retries

agenda.define("send email", async (job) => {
  const { to, subject, body } = job.attrs.data

  try {
    await sendEmail(to, subject, body)
  } catch (err) {
    // Fail the job — agenda records the error:
    throw err
  }
})

// Listen for job events:
agenda.on("success:send email", (job) => {
  console.log(`Email sent to ${job.attrs.data.to}`)
})

agenda.on("fail:send email", (err, job) => {
  console.error(`Failed to send email to ${job.attrs.data.to}:`, err.message)
  // Agenda stores the failure in MongoDB — you can query failed jobs
})

// Manually retry failed jobs:
const failedJobs = await agenda.jobs({ failCount: { $gt: 0 }, name: "send email" })
for (const job of failedJobs) {
  await job.run()
}

Query and manage jobs

// Find all scheduled jobs:
const allJobs = await agenda.jobs({})

// Find specific jobs:
const emailJobs = await agenda.jobs({
  name: "send email",
  "data.to": "user@example.com",
})

// Cancel jobs:
await agenda.cancel({ name: "send email", "data.userId": "user_123" })

// Purge completed jobs older than 7 days:
await agenda.purge()  // Removes completed, failed jobs

// Get job stats:
const pending = await agenda.jobs({ nextRunAt: { $lte: new Date() } })
console.log(`${pending.length} jobs pending`)

Feature Comparison

Featurenode-cronnode-scheduleAgenda
Persistence❌ In-memory❌ In-memory✅ MongoDB
Survives restart
Distributed (multiple servers)✅ (locking)
Cron syntax
One-off scheduling
Job history/logs
Retry failed jobs
Job concurrency control
Requires MongoDB
TypeScript
Weekly downloads~3M~2M~300K

When to Use Each

Choose node-cron if:

  • Simple recurring tasks in a single-process app
  • Tasks are idempotent — safe to re-run from scratch after restart
  • No need for job history, retries, or distributed coordination
  • Minimal setup: just npm install node-cron

Choose node-schedule if:

  • Need to schedule one-off tasks at specific future dates/times
  • Prefer recurrence rule API over cron syntax
  • Simple use case, single process, no persistence needed

Choose Agenda if:

  • Production app where jobs must survive restarts
  • Running multiple server instances (horizontally scaled)
  • Need job failure tracking and retry logic
  • Want a job management interface (see agenda-rest or Agendash UI)
  • Critical jobs (emails, payments, notifications) that cannot be dropped

Consider BullMQ instead if:

  • Already using Redis in your stack (better performance than MongoDB for queues)
  • Need high-throughput job processing (millions of jobs)
  • Want more sophisticated queue patterns (rate limiting, delayed jobs, repeatable jobs)

Methodology

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

Compare task automation and scheduling packages on PkgPulse →

Comments

Stay Updated

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