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:
| Library | What it does | When to use |
|---|---|---|
| node-cron | In-process scheduler, standard cron syntax + seconds | Simple jobs in a single Node process |
| cron-parser | Parses expressions, computes next-run times | Display schedules, compute durations |
| node-schedule | In-process scheduler, cron + Date-based triggers | When you need both cron and one-off dates |
| cron (kelektiv/node-cron) | Older alternative to node-cron | Legacy projects; node-cron has overtaken it |
| agenda | MongoDB-backed persistent job queue with cron | Restart-resilient, single-DB Node deployments |
| bull / BullMQ | Redis-backed job queue with repeatable jobs | Multi-process, multi-server, queue-first apps |
| node-cron-job | Lesser-used, similar to node-cron | Avoid 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 withdistributed: 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 control —
new Worker(..., { concurrency: 5 })caps in-flight jobs - Rate limiting —
limiter: { 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 60at 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?
Why don't my Node scheduled jobs run after a restart?
How do I prevent the same scheduled job from running twice when I have multiple Node instances?
Can node-cron handle timezones?
What's the difference between node-cron and cron-parser?
Related
Every 5 Minutes
`*/5 * * * *` — the most common Node.js scheduled-job interval.
ToolCron Parser
Verify your cron expression before adding it to code.
GuidePython Cron Scheduling
APScheduler, schedule, Celery Beat — the Python equivalents.
GuideKubernetes CronJob
Run scheduled Node.js work as a K8s CronJob instead of in-process.
GuideCloudflare Workers Cron Triggers
Server-less Node-flavored scheduling — cron triggers on Workers.