# Node.js Schedulers Compared: cron, node-cron, node-schedule, croner, Agenda, BullMQ

By Dan Ochoa · Published 2026-10-30 · Updated 2026-10-30
Canonical: https://cronradar.com/blog/node-js-schedulers-compared

> cron, node-cron, node-schedule, croner, Agenda, and BullMQ compared on cron syntax, time zones, persistence, overlap, multiple instances, and releases.

**cron, node-cron, node-schedule, and croner keep schedules in your process's memory: a restart loses them, and each instance of the app runs its own copy unless you add coordination. Agenda and BullMQ store schedules in a database or Redis, so they survive restarts and many workers can share one schedule.** Pick an in-process library for single-instance apps where a missed run is acceptable, and a persistent one when runs must survive deploys or the app runs on several instances.

This guide covers cron 4.4.0, node-cron 4.6.0, node-schedule 2.1.2, croner 11.0.0, Agenda 6.2.6, and BullMQ 6.5.2, each the current release on the [npm registry](https://registry.npmjs.org/) (checked October 30, 2026). For error handling, restarts, and overlaps in node-cron and node-schedule, see [Node.js cron job monitoring](https://cronradar.com/blog/node-js-cron-monitoring); for BullMQ Job Schedulers in depth, see [BullMQ repeatable jobs and Job Schedulers](https://cronradar.com/blog/bullmq-repeatable-jobs). The .NET equivalent of this comparison is [Hangfire vs Quartz.NET vs BackgroundService](https://cronradar.com/blog/hangfire-vs-quartz-vs-backgroundservice).

## Two Models: Timers in the Process, or Jobs in a Store

The first four libraries are timers. You call a function with a cron pattern, the library sets a timer for the next match, and the job is a JavaScript object. node-schedule's README says it plainly: jobs "will only fire as long as your script is running, and the schedule will disappear when execution completes" ([README at v2.1.2](https://github.com/node-schedule/node-schedule/blob/v2.1.2/README.md)). croner "operates in-memory, with no need for a database" ([README at 11.0.0](https://github.com/Hexagon/croner/blob/11.0.0/README.md)), and node-cron "coordinates but does not persist state to a database" ([README at v4.6.0](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md)).

Agenda and BullMQ are job systems. The schedule is a record in a store, workers poll or subscribe to it, and the process that runs a job need not be the one that created the schedule. Agenda 6 has pluggable backends for MongoDB, PostgreSQL, and Redis, each a separate package ([README at agenda@6.2.6](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/README.md)). A BullMQ Job Scheduler lives in Redis, the default backend, or in PostgreSQL with the optional PostgreSQL backend ([PostgreSQL backend guide at v6.5.2](https://github.com/taskforcesh/bullmq/blob/v6.5.2/docs/gitbook/guide/postgresql.md)), and produces each run as a job that waits in the delayed state until it is due ([Job Schedulers docs](https://docs.bullmq.io/guide/job-schedulers/)).

## Node.js Schedulers at a Glance

|                                   | cron (kelektiv)                                                                                                                       | node-cron                                                                                                                                                               | node-schedule                                                                                                                   | croner                                                                                                                    | Agenda                                                                                                                                                                                                                                                                             | BullMQ                                                                                                                                                                                                                                                                                                                                                            |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Latest release                    | 4.4.0, December 9, 2025 ([npm](https://registry.npmjs.org/cron))                                                                      | 4.6.0, July 5, 2026 ([npm](https://registry.npmjs.org/node-cron))                                                                                                       | 2.1.2, October 24, 2026 ([npm](https://registry.npmjs.org/node-schedule))                                                       | 11.0.0, October 26, 2026 ([npm](https://registry.npmjs.org/croner))                                                       | 6.2.6, July 21, 2026 ([npm](https://registry.npmjs.org/agenda))                                                                                                                                                                                                                    | 6.5.2, October 30, 2026 ([npm](https://registry.npmjs.org/bullmq))                                                                                                                                                                                                                                                                                                |
| Weekly downloads, Oct 23-29, 2026 | 10,026,652 ([api](https://api.npmjs.org/downloads/point/last-week/cron))                                                              | 6,769,583 ([api](https://api.npmjs.org/downloads/point/last-week/node-cron))                                                                                            | 4,753,915 ([api](https://api.npmjs.org/downloads/point/last-week/node-schedule))                                                | 14,035,929 ([api](https://api.npmjs.org/downloads/point/last-week/croner))                                                | 228,689 ([api](https://api.npmjs.org/downloads/point/last-week/agenda))                                                                                                                                                                                                            | 13,183,574 ([api](https://api.npmjs.org/downloads/point/last-week/bullmq))                                                                                                                                                                                                                                                                                        |
| Seconds field                     | Optional sixth field at the front ([README](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md))                             | Optional sixth field at the front ([README](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md))                                                              | Optional sixth field at the front ([README](https://github.com/node-schedule/node-schedule/blob/v2.1.2/README.md))              | Optional second, plus optional seventh field for year ([README](https://github.com/Hexagon/croner/blob/11.0.0/README.md)) | Patterns parsed by cron-parser ([nextRunAt.ts](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/src/utils/nextRunAt.ts))                                                                                                                                       | Optional sixth field at the front ([repeat strategies](https://docs.bullmq.io/guide/job-schedulers/repeat-strategies))                                                                                                                                                                                                                                            |
| Extensions                        | Names for months and weekdays; no `L`, `W`, or `#` listed ([README](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md))     | `L`, `L-n`, `W`, `LW`, `#`, `5L`, `?` ([README](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md))                                                          | `#`; no `L` or `W` ([README](https://github.com/node-schedule/node-schedule/blob/v2.1.2/README.md))                             | `L`, `W`, `#`, `?`, `+` for AND ([README](https://github.com/Hexagon/croner/blob/11.0.0/README.md))                       | Also human intervals such as `'1 hour'` ([README](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/README.md))                                                                                                                                                 | Also `every` in milliseconds ([repeat strategies](https://docs.bullmq.io/guide/job-schedulers/repeat-strategies))                                                                                                                                                                                                                                                 |
| Time zone                         | `timeZone` or `utcOffset`; default local ([README](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md))                      | `timezone` option ([README](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md))                                                                              | `tz` on a `RecurrenceRule` ([README](https://github.com/node-schedule/node-schedule/blob/v2.1.2/README.md))                     | `timezone` or `utcOffset` ([README](https://github.com/Hexagon/croner/blob/11.0.0/README.md))                             | `timezone` option on `every()` ([index.ts](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/src/index.ts))                                                                                                                                                     | `tz` repeat option ([repeat options](https://docs.bullmq.io/guide/job-schedulers/repeat-options))                                                                                                                                                                                                                                                                 |
| Persistence                       | None documented; jobs are `CronJob` objects ([README](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md))                   | None ([README](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md))                                                                                           | None ([README](https://github.com/node-schedule/node-schedule/blob/v2.1.2/README.md))                                           | None, in memory ([README](https://github.com/Hexagon/croner/blob/11.0.0/README.md))                                       | MongoDB, PostgreSQL, or Redis backend ([README](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/README.md))                                                                                                                                                   | Redis by default, or PostgreSQL ([PostgreSQL backend](https://github.com/taskforcesh/bullmq/blob/v6.5.2/docs/gitbook/guide/postgresql.md))                                                                                                                                                                                                                        |
| Late or missed runs               | `threshold`: a late tick within it runs, beyond it is skipped ([README](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md)) | `execution:missed` for each slot the timer woke too late to run ([plan-beat.ts](https://github.com/node-cron/node-cron/blob/v4.6.0/src/scheduler/plan-beat.ts#L52-L64)) | Schedule is gone when the script stops ([README](https://github.com/node-schedule/node-schedule/blob/v2.1.2/README.md))         | Not documented ([README](https://github.com/Hexagon/croner/blob/11.0.0/README.md))                                        | One stored job per `every()` name; next run computed from `lastRunAt` ([index.ts](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/src/index.ts), [nextRunAt.ts](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/src/utils/nextRunAt.ts)) | One delayed job per scheduler, so an overdue run happens once ([Job Schedulers](https://docs.bullmq.io/guide/job-schedulers/)); the next run is computed from now ([job-scheduler.ts](https://github.com/taskforcesh/bullmq/blob/v6.5.2/src/classes/job-scheduler.ts#L180-L201))                                                                                  |
| Overlap protection                | `waitForCompletion` skips ticks while one runs ([README](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md))                | `noOverlap` ([README](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md))                                                                                    | None documented ([README](https://github.com/node-schedule/node-schedule/blob/v2.1.2/README.md))                                | `protect` ([README](https://github.com/Hexagon/croner/blob/11.0.0/README.md))                                             | Job locks with `lockLifetime`, per-job `concurrency` ([README](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/README.md))                                                                                                                                    | None built in for a scheduler: the next run is created when the current one starts, so runs can overlap with several workers or `concurrency` above 1 ([Job Schedulers](https://docs.bullmq.io/guide/job-schedulers/)); use a dedicated queue with `queue.setGlobalConcurrency(1)` ([global concurrency](https://docs.bullmq.io/guide/queues/global-concurrency)) |
| Multiple instances                | No coordination documented ([README](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md))                                    | `distributed: true`, env-var flag or Redis coordinator ([README](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md))                                         | README points to Agenda or Bree for multi-node ([README](https://github.com/node-schedule/node-schedule/blob/v2.1.2/README.md)) | No coordination documented ([README](https://github.com/Hexagon/croner/blob/11.0.0/README.md))                            | Shared store with locking ([README](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/README.md))                                                                                                                                                               | Shared Redis or PostgreSQL; schedules live in the store, not in each process ([PostgreSQL backend](https://github.com/taskforcesh/bullmq/blob/v6.5.2/docs/gitbook/guide/postgresql.md))                                                                                                                                                                           |

## Cron Dialects Differ More Than They Look

All six accept a five-field crontab pattern, but the same string does not always mean the same thing.

**Day-of-month and day-of-week.** In standard cron, `0 9 1 * 1` runs on the 1st of the month and on every Monday: when both day fields are restricted, a match on either one is enough ([crontab(5)](https://man7.org/linux/man-pages/man5/crontab.5.html)). croner keeps that OR logic by default and offers `+MON` or `{ domAndDow: true }` for AND ([README at 11.0.0](https://github.com/Hexagon/croner/blob/11.0.0/README.md)). node-cron 4.6.0 combines the two fields with AND: both must match ([README at v4.6.0](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md)). A pattern moved from crontab to node-cron can run on far fewer days. Avoid restricting both fields when a schedule has to be portable.

**Seconds.** A six-field pattern puts seconds first in cron, node-cron, node-schedule, croner, and BullMQ. kelektiv's cron defaults a missing seconds field to 0 ([README at v4.4.0](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md)), and croner's `mode` option can force 5-, 6-, or 7-part parsing instead of detecting it ([README at 11.0.0](https://github.com/Hexagon/croner/blob/11.0.0/README.md)).

**Weekday numbers.** node-cron borrows `L`, `W`, `#`, and `?` from Quartz but keeps standard weekday numbering (`1` is Monday), so Quartz expressions do not carry over unchanged ([README at v4.6.0](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md)). croner switches to Quartz numbering only with `alternativeWeekdays: true` ([README at 11.0.0](https://github.com/Hexagon/croner/blob/11.0.0/README.md)). In kelektiv's cron 4.x, months are numbered 1-12; the README's migration section notes they were 0-11 before ([README at v4.4.0](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md)).

For the fields themselves, see [cron expression syntax](https://cronradar.com/blog/cron-expression).

## Time Zones and Daylight Saving Time

kelektiv's cron runs in local time unless you pass `timeZone` ([README at v4.4.0](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md)). BullMQ passes its repeat options, including `tz`, to cron-parser ([job-scheduler.ts](https://github.com/taskforcesh/bullmq/blob/v6.5.2/src/classes/job-scheduler.ts#L517-L540)), and cron-parser 5.10.2, the version [bullmq 6.5.2 depends on](https://registry.npmjs.org/bullmq/6.5.2), uses the process's local zone when no zone is given ([CronDate.ts](https://github.com/harrisiirak/cron-parser/blob/v5.10.2/src/CronDate.ts#L34-L48)). Set `tz` as a [repeat option](https://docs.bullmq.io/guide/job-schedulers/repeat-options); the [BullMQ guide](https://cronradar.com/blog/bullmq-repeatable-jobs) has more. Set the zone explicitly in every library.

The documented daylight saving behavior differs:

- **node-cron:** in a fall-back the repeated hour runs once, so a `*/15` schedule can pause for up to the length of the shift. The README recommends `timezone: 'UTC'` for an uninterrupted interval ([README at v4.6.0](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md)).
- **croner:** fixed-time jobs such as `0 30 2 * * *` run once at the first occurrence of a repeated time and, for a time that doesn't exist, at the first valid time after the gap. Interval jobs run on both occurrences of a repeated time and skip times that don't exist ([README at 11.0.0](https://github.com/Hexagon/croner/blob/11.0.0/README.md)).

The general problem is covered in [cron and daylight saving time](https://cronradar.com/blog/cron-daylight-saving-time). Running in UTC avoids it.

## What Happens After a Restart or Downtime

For the four in-process libraries, a run that falls due while the process is down never happens, and nothing records that it didn't. The late-run features cover a running process that woke up late: kelektiv's `threshold` decides whether a late tick still runs ([README at v4.4.0](https://github.com/kelektiv/node-cron/blob/v4.4.0/README.md)), and node-cron emits `execution:missed` for each slot its timer woke more than `missedExecutionTolerance` (1000 ms by default) after ([scheduled-task.ts](https://github.com/node-cron/node-cron/blob/v4.6.0/src/tasks/scheduled-task.ts#L88-L95), [plan-beat.ts](https://github.com/node-cron/node-cron/blob/v4.6.0/src/scheduler/plan-beat.ts#L52-L64) at v4.6.0).

Agenda's `every()` saves a single job record for the name (`type = 'single'` in [index.ts](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/src/index.ts)) and computes the next run from `lastRunAt` with cron-parser ([nextRunAt.ts](https://github.com/agenda/agenda/blob/agenda%406.2.6/packages/agenda/src/utils/nextRunAt.ts)). Because there is one record with one next run time, missed runs cannot pile up as separate jobs. BullMQ behaves similarly: the overdue job runs once, late, and the next run is calculated from the current time, for a pattern the next occurrence after now ([job-scheduler.ts at v6.5.2](https://github.com/taskforcesh/bullmq/blob/v6.5.2/src/classes/job-scheduler.ts#L180-L201)) and for `every` on Redis the next slot after now ([getJobSchedulerEveryNextMillis.lua](https://github.com/taskforcesh/bullmq/blob/v6.5.2/src/commands/includes/getJobSchedulerEveryNextMillis.lua#L22-L38)). The [BullMQ guide](https://cronradar.com/blog/bullmq-repeatable-jobs) covers this in depth.

Neither replays every missed slot. If each slot matters, for example a report per day, make the job find and process the gap itself; see [missed runs after downtime](https://cronradar.com/blog/missed-runs-after-downtime).

## Overlap and Multiple Instances

Three in-process libraries can skip a run while the previous one is still going: `waitForCompletion` in kelektiv's cron, `noOverlap` in node-cron, and `protect` in croner. node-schedule has none, so track a running flag yourself ([example](https://cronradar.com/blog/node-js-cron-monitoring)). These options work within one process only. Two instances of the app are two schedulers, and each runs every job; see [preventing overlapping cron jobs](https://cronradar.com/blog/prevent-overlapping-cron-jobs) for locks that span processes.

node-cron 4.6.0 is the only in-process library here with built-in coordination: `distributed: true` makes one instance run each firing, using an environment-variable flag by default or a Redis coordinator for high availability ([README at v4.6.0](https://github.com/node-cron/node-cron/blob/v4.6.0/README.md)). Agenda and BullMQ coordinate through their store by design. For the patterns behind all of this, see [distributed cron](https://cronradar.com/blog/distributed-cron).

## How to Choose

- **One instance, missed runs tolerable, standard crontab patterns:** kelektiv's `cron` or croner. croner adds `L`, `W`, `#`, a year field, and documented DST rules, and keeps standard OR semantics for day fields.
- **One instance now, several later, without a database:** node-cron 4, for `noOverlap`, `distributed`, and lifecycle events. Remember its AND rule for day-of-month plus day-of-week.
- **Calendar-style rules more than cron strings:** node-schedule's `RecurrenceRule`, with an `error` listener on every job ([why](https://cronradar.com/blog/node-js-cron-monitoring)).
- **Schedules must survive deploys and run on one of many workers:** Agenda on MongoDB, PostgreSQL, or Redis, or BullMQ on Redis or PostgreSQL. BullMQ if you need a work queue too.

## Monitoring Whichever You Pick

None of these libraries can tell you about a run that never started: a stopped process, a worker that never came up, or a schedule removed from the store all fail without an error. An outside monitor that knows the schedule catches those.

CronRadar's `cronradar` npm package is unavailable, so Node.js jobs report over HTTP check-ins. Check-ins do not discover jobs automatically: each job reports with its own monitor key and its schedule. The [Node.js monitoring guide](https://cronradar.com/blog/node-js-cron-monitoring) has a `monitored(monitorKey, schedule, task)` wrapper that sends start, complete, and fail check-ins with HTTP Basic authentication (the API key as the username, empty password), a five-second timeout, and the key read from `CRONRADAR_API_KEY` in the environment. It wraps any job function, so it works the same way with each library:

```javascript
const { Cron } = require('croner')

// monitored() is the wrapper from the Node.js monitoring guide
new Cron(
  '0 3 * * *',
  { timezone: 'UTC', protect: true },
  monitored('nightly-backup', '0 3 * * *', performBackup),
)
```

```javascript
const { CronJob } = require('cron')

CronJob.from({
  cronTime: '0 3 * * *',
  onTick: monitored('nightly-backup', '0 3 * * *', performBackup),
  timeZone: 'UTC',
  waitForCompletion: true,
  start: true,
})
```

For BullMQ, check in from the worker's processor, as shown in the [BullMQ guide](https://cronradar.com/blog/bullmq-repeatable-jobs).

CronRadar reads schedules as standard five-field cron evaluated in UTC ([schedule syntax](https://docs.cronradar.com/schedules)). Run the job in UTC and send a five-field pattern: drop a leading seconds field (`0 0 2 * * *` becomes `0 2 * * *`). Five fields cannot express `L` or `W`, so schedule such a job on every candidate date and let it decide whether to do the work.

A monitor is `pending` until its first check-in and `healthy` while runs arrive. A late run is `warning` inside its grace period and `critical` once grace expires; a failure check-in makes it `critical` straight away. Alerts go to email, Slack, webhook, Discord, or PagerDuty.

## Frequently Asked Questions

### What is the difference between cron and node-cron on npm?

They are separate projects. `cron` is maintained in the kelektiv/node-cron repository and exports `CronJob`; `node-cron` is maintained in the node-cron/node-cron repository and exports `schedule()`. Their dialects and options differ: for example, node-cron combines day-of-month and day-of-week with AND.

### Which Node.js scheduler supports seconds?

cron, node-cron, node-schedule, croner, and BullMQ document an optional seconds field at the front of a six-field pattern, and croner also accepts a seventh field for the year. Agenda passes cron patterns to cron-parser.

### Which Node.js schedulers keep jobs across restarts?

Agenda, which stores jobs in MongoDB, PostgreSQL, or Redis, and BullMQ, which stores Job Schedulers in Redis or PostgreSQL. cron, node-cron, node-schedule, and croner keep schedules in memory and lose them when the process stops.

### Do Agenda and BullMQ run every missed job after downtime?

No. Each keeps one pending run per schedule, so after downtime the overdue run happens once and the next is calculated from there. Make the job process the missed period itself if every slot matters.

### Does CronRadar integrate with node-cron, croner, Agenda, or BullMQ?

No. The Node.js SDK is unavailable, and CronRadar does not discover Node.js jobs. Each job reports through HTTP check-ins with its own monitor key and schedule.
