cronuru
Guide

Node.js Cron Jobs — A Complete Guide

Node.js doesn't have a built-in scheduler — there are six or seven popular libraries that do the job, and which one you pick depends on whether you need in-process simplicity (`node-cron`), expression parsing (`cron-parser`), durable persistent jobs (`agenda`), or a full Redis-backed queue (`bullmq`). This guide walks through the trade-offs.

Updated

The library landscape

Node.js has no built-in scheduler. The mature options:

LibraryWhat it doesWhen to use
node-cronIn-process scheduler, standard cron syntax + secondsSimple jobs in a single Node process
cron-parserParses expressions, computes next-run timesDisplay schedules, compute durations
node-scheduleIn-process scheduler, cron + Date-based triggersWhen you need both cron and one-off dates
cron (kelektiv/node-cron)Older alternative to node-cronLegacy projects; node-cron has overtaken it
agendaMongoDB-backed persistent job queue with cronRestart-resilient, single-DB Node deployments
bull / BullMQRedis-backed job queue with repeatable jobsMulti-process, multi-server, queue-first apps
node-cron-jobLesser-used, similar to node-cronAvoid unless you have a specific reason

The choice usually splits along two axes:

  • In-process vs distributed — does your scheduler need to survive restarts and coordinate across multiple Node processes?
  • Cron-only vs cron-plus-queue — do you just need “fire this function on a schedule” or do you want retry policies, concurrency control, and observability that come with a job queue?

node-cron

The smallest, simplest option. Standard Unix 5-field cron syntax with optional seconds prefix.

npm install node-cron
import cron from "node-cron";

// Every 5 minutes
cron.schedule("*/5 * * * *", () => {
  console.log("Running every 5 minutes:", new Date().toISOString());
});

// With timezone
cron.schedule(
  "0 9 * * 1-5",
  () => sendDailyDigest(),
  { timezone: "America/New_York" }
);

// With seconds (6-field)
cron.schedule("*/30 * * * * *", () => heartbeat());

The returned task can be controlled. Note that in node-cron v4 (the current major, a TypeScript rewrite), cron.schedule() always starts immediately — the old { scheduled: false } option was removed. To create a task that starts stopped, use cron.createTask() and start it yourself:

const task = cron.createTask("*/5 * * * *", doWork); // created, not started
task.start();
task.stop();
task.destroy();

What node-cron does well:

  • Tiny dependency footprint
  • Standard cron syntax — see the Unix cron dialect reference
  • Timezone support via IANA names
  • Graceful start/stop/destroy
  • Overlap prevention with noOverlap: true, and single-fire coordination across instances with distributed: true (both added in v4)

What it still doesn’t do:

  • Survive process restarts (everything is in-memory)
  • Retry on failure

cron-parser

cron-parser parses a cron expression and computes next-run times. It does not run jobs. Use it when you need the metadata:

npm install cron-parser
import { CronExpressionParser } from "cron-parser";

const interval = CronExpressionParser.parse("*/5 * * * *", {
  tz: "America/Los_Angeles",
});

console.log("Next:", interval.next().toDate());
console.log("Next:", interval.next().toDate());

// Or compute many at once
const nextFive = [];
for (let i = 0; i < 5; i++) nextFive.push(interval.next().toDate());

Common use cases:

  • Display “next run” in an admin UI
  • Compute how long until the next scheduled fire
  • Validate user-submitted cron expressions before saving them
  • Calculate frequency (“how many times per day does this fire?”)

The Cronuru parser tool is built on cron-parser under the hood — paste any expression to see what it produces.

node-schedule

A more flexible in-process scheduler that handles both cron expressions and one-off Date triggers.

npm install node-schedule
import schedule from "node-schedule";

// Cron-style
const job1 = schedule.scheduleJob("0 9 * * 1-5", () => sendStandup());

// One-off Date
const tomorrowNoon = new Date(Date.now() + 86400 * 1000);
tomorrowNoon.setHours(12, 0, 0, 0);
schedule.scheduleJob(tomorrowNoon, () => sendReminder());

// Recurrence rule (alternative to cron syntax)
const rule = new schedule.RecurrenceRule();
rule.dayOfWeek = [new schedule.Range(1, 5)];
rule.hour = 9;
rule.minute = 0;
rule.tz = "America/New_York";
schedule.scheduleJob(rule, () => doDailyWork());

Use node-schedule over node-cron when:

  • You need to schedule one-off events at specific Dates
  • You want the RecurrenceRule API instead of cron strings
  • You need more flexible scheduling like “the last weekday of every month at 5 PM”

Same restart and multi-instance caveats as node-cron — both are in-memory.

agenda

A persistent job queue backed by MongoDB, with cron-style scheduling.

npm install @hokify/agenda
import { Agenda } from "@hokify/agenda";

const agenda = new Agenda({ db: { address: "mongodb://localhost/agenda" } });

agenda.define("send digest", async (job) => {
  await sendDailyDigest();
});

await agenda.start();

// Schedule with cron syntax
agenda.every("0 9 * * 1-5", "send digest");

// Or with interval syntax
agenda.every("15 minutes", "refresh cache");

agenda persists job definitions and run history to MongoDB. After a restart, scheduled jobs resume without losing the schedule. It also handles:

  • Locking so the same scheduled run isn’t picked up by two workers simultaneously
  • Retries with configurable backoff
  • Job history queryable from MongoDB
  • Multiple workers — run agenda in N Node processes and they coordinate via MongoDB

Use agenda when you already have MongoDB in your stack and want persistence without adding Redis.

BullMQ repeatable jobs

BullMQ is a Redis-backed job queue with first-class support for cron-style repeating jobs.

npm install bullmq
import { Queue, Worker } from "bullmq";

const connection = { host: "localhost", port: 6379 };
const queue = new Queue("reports", { connection });

// Add a repeatable job
await queue.add(
  "daily-digest",
  { audience: "team" },
  {
    repeat: {
      pattern: "0 9 * * 1-5",       // standard cron syntax
      tz: "America/New_York",
    },
  }
);

// Worker process picks up scheduled fires
new Worker(
  "reports",
  async (job) => {
    if (job.name === "daily-digest") {
      await sendDigest(job.data.audience);
    }
  },
  { connection }
);

What BullMQ does that simpler libraries don’t:

  • Redis-backed durability — schedule survives restarts
  • Workers scale horizontally — add N worker processes, jobs distribute automatically
  • Retry with exponential backoff out of the box
  • Concurrency controlnew Worker(..., { concurrency: 5 }) caps in-flight jobs
  • Rate limitinglimiter: { max: 100, duration: 60000 } for “max 100 per minute”
  • Job observability via BullMQ’s API or the bull-board UI

The trade-off: you need Redis. For apps that already use Redis (caching, sessions, pub/sub), this is free. For new projects, weigh the operational cost against the features.

Production concerns

Whether you use node-cron or BullMQ, the same handful of issues show up in production:

Multi-instance fires. If you run three Node processes (a typical cluster behind a load balancer), an in-process scheduler fires on all three. Either:

  • Gate the scheduler on process.env.SCHEDULER_LEADER === "true" and set the env var on exactly one instance
  • Use a distributed lock — Redis SET key value NX EX 60 at the start of each scheduled run
  • Use a scheduler that coordinates automatically (agenda, bullmq)

Don’t run schedulers in dev. Wrap the schedule registration in if (process.env.NODE_ENV === "production") { ... } or you’ll wake up to dev-server emails sent at 9 AM every weekday.

Graceful shutdown. When the process gets SIGTERM, give in-flight scheduled jobs time to finish:

const tasks = [];

process.on("SIGTERM", async () => {
  await Promise.all(tasks.map((t) => t.stop()));
  process.exit(0);
});

For BullMQ:

await worker.close();   // waits for active jobs to finish

Long-running schedules block the event loop. Node is single-threaded. A scheduled job that does heavy CPU work blocks all incoming HTTP requests on the same process. For CPU-bound work, move it to a worker thread or a separate worker process.

Logging and observability. In production, log:

  • When each scheduled fire starts (with the cron expression that triggered it)
  • When it completes (with duration)
  • Any errors with stack traces

For BullMQ specifically, the failed and completed events give you hooks for metrics.

Choosing the right one

Quick decision tree:

  • Single Node process, no persistence needed?node-cron
  • Need to compute next-run times, not actually run jobs?cron-parser
  • Need one-off Date triggers in addition to cron?node-schedule
  • Multi-process, MongoDB already in stack?agenda
  • Multi-process or queue-first, Redis already in stack?bullmq
  • High operational requirements (SLA, monitoring, audit)? → Move scheduling out of Node entirely. Use Kubernetes CronJob, AWS EventBridge Scheduler, or Cloudflare Workers cron triggers to invoke a Node HTTP endpoint on a schedule. This decouples scheduling from your app’s runtime and gives you the operational guarantees of a real scheduler.

The pattern of “external scheduler + simple HTTP endpoint in Node” is increasingly common — your Node app stays stateless and horizontally scalable, the scheduler handles all the failure modes, and switching schedulers later doesn’t touch app code.

Frequently asked questions

Which Node.js cron library should I use?
For in-process jobs in a single Node process: `node-cron` (simple) or `node-schedule` (slightly more features). For computing next-run times from a cron string: `cron-parser`. For durable persistent jobs across restarts: `agenda` (MongoDB) or `bullmq` (Redis). For distributed scheduling across many Node processes: BullMQ repeatable jobs.
Why don't my Node scheduled jobs run after a restart?
In-process schedulers like `node-cron` and `node-schedule` keep their schedule in memory. When the process exits (deploy, crash, OOM kill), the schedule is lost and any missed firings are gone forever. For restart-resilient scheduling, use `agenda` (persists to MongoDB) or `bullmq` (persists to Redis), or move the scheduling out of Node entirely into Kubernetes CronJob, EventBridge, or system cron.
How do I prevent the same scheduled job from running twice when I have multiple Node instances?
In-process schedulers will fire on every instance — three Node pods running `*/5 * * * *` means three invocations every 5 minutes. Either: (1) only enable the scheduler on one instance via env var, (2) acquire a distributed lock (Redis SETNX with TTL) at the start of each scheduled run, or (3) use a coordinator like BullMQ that handles deduplication for you.
Can node-cron handle timezones?
Yes. `cron.schedule(expr, fn, { timezone: 'America/New_York' })` interprets the cron expression in the given IANA timezone, including DST transitions. Without the timezone option, schedules run in the server's local timezone.
What's the difference between node-cron and cron-parser?
`node-cron` actually runs jobs on a schedule — you pass it a function and it invokes the function at the scheduled times. `cron-parser` only parses a cron expression and tells you when it would fire — it doesn't run anything. Use cron-parser when you want to display next-run times or compute durations; use node-cron when you actually want code to run.