agenda、bree、bull、cron、later、node-cron 和 node-schedule 都是用于在 Node.js 环境中执行定时任务或延迟作业的库,但它们的架构理念、依赖环境和适用场景截然不同。cron、node-cron 和 node-schedule 专注于基于 Cron 表达式的内存级定时触发,适合简单的周期性任务;later 提供更灵活的自然语言式调度逻辑但已停止维护;agenda 和 bull 是基于数据库(MongoDB 和 Redis)的重型作业队列,支持持久化、重试和分布式处理;bree 则是一个现代化的轻量级包装器,旨在简化原生 worker_threads 和 Cron 的使用,强调隔离性和易用性。
在 Node.js 生态中,处理定时任务(Scheduled Jobs)和背景作业(Background Jobs)有多种方案。开发者常面临的选择困境是:究竟该用简单的内存定时器,还是引入沉重的数据库依赖?本文将深入对比 agenda、bree、bull、cron、later、node-cron 和 node-schedule,帮助你根据实际工程需求做出架构决策。
这是选择库时的第一个分水岭。cron、node-cron、node-schedule 和 later 将调度信息保存在内存中。这意味着如果 Node.js 进程重启,所有待执行的任务都会丢失。相反,agenda 和 bull 将任务存储在数据库(MongoDB 或 Redis)中,确保即使服务崩溃,任务也能在恢复后继续执行。
node-cron 是典型的内存型库,它解析 Cron 表达式并在内存中设置定时器。
// node-cron: 内存级定时,进程重启后任务丢失
const cron = require('node-cron');
cron.schedule('*/5 * * * *', () => {
console.log('每 5 分钟运行一次,重启后需重新注册');
});
bull 则将作业推入 Redis 队列,即使进程挂掉,作业依然安全存储在 Redis 中。
// bull: 持久化队列,进程重启后作业仍可处理
const Queue = require('bull');
const myQueue = new Queue('my-tasks', 'redis://127.0.0.1:6379');
myQueue.add({ foo: 'bar' }, { repeat: { cron: '*/5 * * * *' } });
// 作业定义存储在 Redis 中,Worker 可随时拉取
agenda 使用 MongoDB 存储作业定义和状态,支持复杂的查询和锁定机制。
// agenda: 基于 MongoDB 的持久化作业
const Agenda = require('agenda');
const agenda = new Agenda({ db: { address: 'mongodb://localhost/agenda' } });
agenda.define('send email', async (job) => {
console.log('发送电子邮件');
});
await agenda.every('5 minutes', 'send email');
// 调度规则存入 MongoDB,支持多实例竞争锁
bree 虽然主要作为包装器,但它支持将作业配置持久化到文件系统或数据库,并结合了 worker_threads。
// bree: 配置文件驱动,支持持久化配置
const Bree = require('bree');
const bree = new Bree({
root: './jobs',
jobs: [
{ name: 'daily-report', interval: '0 0 * * *' } // 支持 cron 字符串
]
});
bree.start();
在单线程的 Node.js 中,长时间运行的任务会阻塞事件循环,导致 API 响应变慢。传统的 node-cron 和 node-schedule 默认在主线程执行回调。而 bree 和 bull 提供了更好的隔离方案。
node-schedule 直接在主线程执行回调函数,若任务耗时过长,会阻塞其他请求。
// node-schedule: 主线程执行,耗时任务会阻塞事件循环
const schedule = require('node-schedule');
schedule.scheduleJob('*/10 * * * *', async () => {
await heavyComputation(); // 阻塞主线程!
});
bree 的核心优势在于默认使用 worker_threads,将任务放在独立的线程中运行,彻底解耦主线程。
// bree: 自动使用 worker_threads 隔离任务
// jobs/daily-report.js (独立文件)
module.exports = async () => {
await heavyComputation(); // 在独立线程运行,不阻塞主进程
};
// 主进程配置
const bree = new Bree({ root: './jobs', jobs: ['daily-report'] });
bull 通过定义独立的 Worker 进程来处理作业,天然支持分布式和进程隔离。
// bull: 独立的 Worker 进程处理作业
// worker.js
const worker = queue.process(async (job) => {
await heavyComputation(); // 在独立进程/线程中运行
});
对于调度频率的定义,不同库提供了不同粒度的控制。
cron 提供最基础的 Cron 解析,严格遵循标准,不支持秒级或非标准表达式。
// cron: 基础解析,通常用于底层
const { CronJob } = require('cron');
new CronJob('0 0 12 * * *', () => {
console.log('中午 12 点执行');
});
node-schedule 扩展了 Cron 语法,支持直接传入 Date 对象或递归规则(RecurrenceRule),灵活性更高。
// node-schedule: 支持 Date 对象和复杂规则
const schedule = require('node-schedule');
// 一次性任务:特定时间点
schedule.scheduleJob(new Date(2023, 11, 31, 23, 59, 59), () => {
console.log('新年快乐');
});
// 递归规则:每周五下午 3 点
const rule = new schedule.RecurrenceRule();
rule.dayOfWeek = 5;
rule.hour = 15;
schedule.scheduleJob(rule, () => {
console.log('周五下午茶时间');
});
later (已弃用) 曾提供极具可读性的自然语言式调度,但现在不再推荐。
// later: 自然语言风格 (已不推荐在新项目使用)
const later = require('later');
const schedule = later.parse.text('every 5 mins on weekdays');
later.setInterval(() => {
console.log('工作日每 5 分钟');
}, schedule);
bree 和 bull 均支持标准的 Cron 字符串,同时也支持人类可读的时间间隔(如 5 seconds)。
// bree: 支持人类可读间隔
const bree = new Bree({
jobs: [
{ name: 'task1', interval: '5 seconds' },
{ name: 'task2', cron: '0 0 * * *' }
]
});
// bull: 重复作业配置
queue.add('task', {}, {
repeat: { cron: '*/5 * * * *' } // 标准 Cron
// 或 repeat: { every: 5000 } // 毫秒间隔
});
在选型时,库的活跃程度至关重要。
later: 已弃用。该库已多年未更新,存在潜在的安全风险和兼容性问题。强烈建议不要在新项目中使用。如果需要灵活的调度语法,请迁移至 node-schedule 或使用 cronstrue 辅助生成表达式。cron: 维护状态一般,功能较为基础,通常作为其他库的依赖存在。除非你需要极小的包体积且只需最基础的功能,否则推荐功能更完善的 node-cron。agenda / bull: 两者都非常活跃,但架构较重。agenda 依赖 MongoDB,bull 依赖 Redis。如果你的基础设施中没有这些数据库,引入它们仅为了定时任务可能得不偿失。bree: 现代、轻量且积极维护。它是目前替代老旧 node-cron + child_process 组合的最佳实践方案。需求:每天凌晨运行一次,无需持久化,代码简单。
node-cron 或 breebree 更能防止阻塞主线程。// 使用 node-cron
cron.schedule('0 0 * * *', () => cleanupLogs());
需求:任务耗时久,不能阻塞 API 响应,失败需重试。
bull 或 agenda// 使用 bull
queue.add('generate-report', { userId: 123 }, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } });
需求:用户可在界面动态设置任意时间的提醒,支持单次和循环。
node-schedule (小规模) 或 agenda (大规模)node-schedule 支持 Date 对象,适合动态添加一次性任务。若需集群支持,则选 agenda。// 使用 node-schedule 动态添加
schedule.scheduleJob(userSelectedDate, () => sendReminder());
| 特性 | node-cron | node-schedule | bree | bull | agenda | later |
|---|---|---|---|---|---|---|
| 存储方式 | 内存 | 内存 | 配置/内存 | Redis | MongoDB | 内存 |
| 进程重启存活 | ❌ | ❌ | ✅ (配置持久化) | ✅ | ✅ | ❌ |
| 执行隔离 | 主线程 | 主线程 | Worker Threads | 独立 Worker | 主线程/Worker | 主线程 |
| 调度语法 | Cron | Cron + Date + Rule | Cron + Interval | Cron + Interval | Cron + Human | Natural Language |
| 重试机制 | ❌ | ❌ | ✅ (基础) | ✅ (高级) | ✅ (高级) | ❌ |
| 分布式支持 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
| 维护状态 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ (弃用) |
对于大多数现代 Node.js 应用,bree 是最平衡的选择:它既解决了主线程阻塞问题,又提供了简单的 Cron 支持,且没有重型数据库依赖。
如果你的业务涉及关键数据流(如支付回调、重要通知),必须保证任务绝对不丢失且支持横向扩展,那么 bull (配合 Redis) 是工业界的标准答案。
尽量避免使用 later,并在需要复杂调度逻辑时优先考虑 node-schedule 而非原始的 cron 包。记住,简单的工具解决简单的问题,复杂的工具解决可靠性的问题 —— 不要为了杀鸡而用牛刀,也不要为了盖楼而只用积木。
仅在对性能有极致要求且不需要人类可读的 Cron 表达式时选择基础的 cron 包。它提供了最底层的 Cron 解析能力,通常作为其他库的底层依赖,对于大多数应用层开发来说,直接使用更上层的封装(如 node-cron)通常是更好的选择。
如果你的项目已经使用 MongoDB 且需要强大的作业持久化、重试机制和分布式处理能力,选择 agenda。它适合需要确保任务不丢失、支持复杂查询筛选作业以及需要管理后台任务队列的企业级应用,但需注意其依赖 MongoDB 带来的运维成本。
避免在新项目中使用 later,因为它已不再积极维护。虽然它曾提供比标准 Cron 更灵活的调度语法(如自然语言解析),但在现代 Node.js 生态中,其功能已被 node-schedule 或更专业的作业队列库所取代,继续使用可能带来兼容性风险。
选择 bree 如果你希望以最小的配置快速启动定时任务,同时利用 Node.js 原生的 worker_threads 实现任务隔离以防止主线程阻塞。它非常适合现代 Node.js 项目,特别是那些希望避免重型依赖(如 Redis/MongoDB)且需要简单 Cron 调度或脚本定期执行的场景。
当你的系统需要高吞吐量、极高的可靠性以及复杂的作业流控制(如优先级、延迟、重复、沙箱处理)时,bull 是最佳选择。基于 Redis 的特性使其在分布式环境和微服务架构中表现卓越,适合处理关键业务逻辑的背景作业,但需承担 Redis 的维护成本。
如果你需要一个零依赖、轻量级且仅用于在单进程中执行简单周期性任务的库,node-cron 是理想选择。它完美兼容标准 Cron 语法,适合日志轮转、健康检查等不需要持久化或分布式的简单场景,但不适合长运行或关键任务。
选择 node-schedule 如果你需要比标准 Cron 更丰富的调度规则(如支持 Date 对象、递归规则)且希望保持代码简洁。它在内存中运行,适合中等复杂度的定时任务,但在进程重启后会丢失任务状态,因此不适用于需要严格保证执行的关键作业。
cron is a robust tool for running jobs (functions or commands) on schedules defined using the cron syntax.
Perfect for tasks like data backups, notifications, and many more!
child_processnpm install cron
v4 dropped Node v16 and renamed the job.running property:
Node v16 is no longer supported. Upgrade your Node installation to Node v18 or above
You can no longer set the running property (now isActive). It is read-only. To start or stop a cron job, use job.start() and job.stop().
v3 introduced TypeScript and tighter Unix cron pattern alignment:
Month Indexing: Changed from 0-11 to 1-12. So you need to increment all numeric months by 1.
Day-of-Week Indexing: Support added for 7 as Sunday.
CronJobCronJob.from(argsObject) instead.nextDates(count?: number) now always returns an array (empty if no argument is provided). Use nextDate() instead for a single date.removed job() method in favor of new CronJob(...args) / CronJob.from(argsObject)
removed time() method in favor of new CronTime()
import { CronJob } from 'cron';
const job = new CronJob(
'* * * * * *', // cronTime
function () {
console.log('You will see this message every second');
}, // onTick
null, // onComplete
true, // start
'America/Los_Angeles' // timeZone
);
// job.start() is optional here because of the fourth parameter set to true.
// equivalent job using the "from" static method, providing parameters as an object
const job = CronJob.from({
cronTime: '* * * * * *',
onTick: function () {
console.log('You will see this message every second');
},
start: true,
timeZone: 'America/Los_Angeles'
});
Note: In the first example above, the fourth parameter to
CronJob()starts the job automatically. If not provided or set to falsy, you must explicitly start the job usingjob.start().
For more advanced examples, check the examples directory.
Cron patterns are the backbone of this library. Familiarize yourself with the syntax:
- `*` Asterisks: Any value
- `1-3,5` Ranges: Ranges and individual values
- `*/2` Steps: Every two units
Detailed patterns and explanations are available at crontab.org. The examples in the link have five fields, and 1 minute as the finest granularity, but our cron scheduling supports an enhanced format with six fields, allowing for second-level precision. Tools like crontab.guru can help in constructing patterns but remember to account for the seconds field.
Here's a quick reference to the UNIX Cron format this library uses, plus an added second field:
field allowed values
----- --------------
second 0-59
minute 0-59
hour 0-23
day of month 1-31
month 1-12 (or names, see below)
day of week 0-7 (0 or 7 is Sunday, or use names)
Names can also be used for the 'month' and 'day of week' fields. Use the first three letters of the particular day or month (case does not matter). Ranges and lists of names are allowed.
Examples: "mon,wed,fri", "jan-mar".
sendAt: Indicates when a CronTime will execute (returns a Luxon DateTime object).
import * as cron from 'cron';
const dt = cron.sendAt('0 0 * * *');
console.log(`The job would run at: ${dt.toISO()}`);
timeout: Indicates the number of milliseconds in the future at which a CronTime will execute (returns a number).
import * as cron from 'cron';
const timeout = cron.timeout('0 0 * * *');
console.log(`The job would run in ${timeout}ms`);
validateCronExpression: Validates if a given cron expression is valid (returns an object with valid and error properties).
import * as cron from 'cron';
const validation = cron.validateCronExpression('0 0 * * *');
console.log(`Is the cron expression valid? ${validation.valid}`);
if (!validation.valid) {
console.error(`Validation error: ${validation.error}`);
}
constructor(cronTime, onTick, onComplete, start, timeZone, context, runOnInit, utcOffset, unrefTimeout, waitForCompletion, errorHandler, name, threshold):
cronTime: [REQUIRED] - The time to fire off your job. Can be cron syntax, a JS Date object or a Luxon DateTime object.
onTick: [REQUIRED] - Function to execute at the specified time. If an onComplete callback was provided, onTick will receive it as an argument.
onComplete: [OPTIONAL] - Invoked when the job is halted with job.stop(). It might also be triggered by onTick post its run.
start: [OPTIONAL] - Determines if the job should commence before constructor exit. Default is false.
timeZone: [OPTIONAL] - Sets the execution time zone. Default is local time. Check valid formats in the Luxon documentation.
context: [OPTIONAL] - Execution context for the onTick method.
runOnInit: [OPTIONAL] - Instantly triggers the onTick function post initialization. Default is false.
utcOffset: [OPTIONAL] - Specifies time zone offset in minutes. Cannot co-exist with timeZone.
unrefTimeout: [OPTIONAL] - Useful for controlling event loop behavior. More details here.
waitForCompletion: [OPTIONAL] - If true, no additional instances of the onTick callback function will run until the current onTick callback has completed. Any new scheduled executions that occur while the current callback is running will be skipped entirely. Default is false.
errorHandler: [OPTIONAL] - Function to handle any exceptions that occur in the onTick method.
name: [OPTIONAL] - Name of the job. Useful for identifying jobs in logs.
threshold: [OPTIONAL] - Threshold in ms to control whether to execute or skip missed execution deadlines caused by slow or busy hardware. Execution delays within threshold will be executed immediately, and otherwise will be skipped. In both cases a warning will be printed to the console with the job name and cron expression. See issue #962 for more information. Default is 250.
from (static): Create a new CronJob object providing arguments as an object. See argument names and descriptions above.
start: Initiates the job.
stop: Halts the job.
setTime: Modifies the time for the CronJob. Parameter must be a CronTime.
lastDate: Provides the last execution date.
nextDate: Indicates the subsequent date that will activate an onTick.
nextDates(count): Supplies an array of upcoming dates that will initiate an onTick.
fireOnTick: Allows modification of the onTick calling behavior.
addCallback: Permits addition of onTick callbacks.
isActive: [READ-ONLY] Indicates if a job is active (checking to see if the callback needs to be called).
isCallbackRunning: [READ-ONLY] Indicates if a callback is currently executing.
const job = new CronJob('* * * * * *', async () => {
console.log(job.isCallbackRunning); // true during callback execution
await someAsyncTask();
console.log(job.isCallbackRunning); // still true until callback completes
});
console.log(job.isCallbackRunning); // false
job.start();
console.log(job.isActive); // true
console.log(job.isCallbackRunning); // false
constructor(time, zone, utcOffset):
time: [REQUIRED] - The time to initiate your job. Accepts cron syntax or a JS Date object.
zone: [OPTIONAL] - Equivalent to timeZone from CronJob parameters.
utcOffset: [OPTIONAL] - Analogous to utcOffset from CronJob parameters.
Both JS Date and Luxon DateTime objects don't guarantee millisecond precision due to computation delays. This module excludes millisecond precision for standard cron syntax but allows execution date specification through JS Date or Luxon DateTime objects. However, specifying a precise future execution time, such as adding a millisecond to the current time, may not always work due to these computation delays. It's observed that delays less than 4-5 ms might lead to inconsistencies. While we could limit all date granularity to seconds, we've chosen to allow greater precision but advise users of potential issues.
Using arrow functions for onTick binds them to the parent's this context. As a result, they won't have access to the cronjob's this context. You can read a little more in issue #47 (comment).
Join the Discord server! Here you can discuss issues and get help in a more casual forum than GitHub.
This project is looking for help! If you're interested in helping with the project, please take a look at our contributing documentation.
Please have a look at our contributing documentation, it contains all the information you need to know before submitting an issue.
This is a community effort project. In the truest sense, this project started as an open source project from cron.js and grew into something else. Other people have contributed code, time, and oversight to the project. At this point there are too many to name here so we'll just say thanks.
Special thanks to Hiroki Horiuchi, Lundarl Gholoi and koooge for their work on the DefinitelyTyped typings before they were imported in v2.4.0.
MIT