agenda、cron、later 和 node-cron 都是用于在 Node.js 环境中实现定时任务调度的 npm 包,但它们的设计目标、持久化能力、时间表达式语法和适用场景有显著差异。agenda 是一个基于 MongoDB 的持久化任务队列系统,支持任务重试、优先级和分布式调度;cron 和 node-cron 提供类似 Unix cron 的语法来定义周期性任务,但前者更轻量且仅支持基础功能,后者则提供更丰富的 API 和错误处理机制;later 专注于灵活的时间调度规则,支持文本和 cron 表达式,并能生成未来执行时间列表,但不直接提供任务执行器。这些库适用于从简单脚本到复杂后台作业系统的不同需求。
在 Node.js 应用中,定时任务无处不在 —— 从每日数据备份到每分钟健康检查。但面对 agenda、cron、later 和 node-cron 这些看似相似的库,如何选型?本文从真实工程角度出发,深入比较它们的核心能力、适用边界和代码实践。
agenda 是唯一内置持久化能力的库。它将任务元数据(如下次运行时间、重试次数)存入 MongoDB,即使服务重启,未完成的任务也不会丢失。
// agenda: 任务自动持久化到 MongoDB
const agenda = new Agenda({ db: { address: 'mongodb://localhost:27017/agenda' } });
agenda.define('send email', async (job) => {
await sendEmail(job.attrs.data.to);
});
// 即使进程退出,任务仍会在指定时间恢复执行
agenda.schedule('in 1 hour', 'send email', { to: 'user@example.com' });
agenda.start();
cron、node-cron 和 later 均为内存调度器。一旦进程终止,所有计划任务立即消失,无法恢复。
// cron: 内存中调度,进程退出即失效
const CronJob = require('cron').CronJob;
new CronJob('0 0 * * *', () => {
console.log('Daily cleanup');
}, null, true);
// node-cron: 同样仅存在于内存
const cron = require('node-cron');
cron.schedule('0 0 * * *', () => {
console.log('Daily cleanup');
});
// later: 仅计算时间,不执行任务,更无持久化
const later = require('later');
const schedule = later.parse.text('at 9:00 am');
// 需自行实现执行逻辑和状态管理
✅ 结论:若任务不能因服务重启而丢失(如支付对账、通知发送),必须选
agenda。否则,内存调度器足够。
cron 和 node-cron 使用标准 Unix cron 语法(5 或 6 位字段),但 node-cron 额外支持秒级精度(6 位)。
// cron: 标准 5 位 cron(分 时 日 月 周)
new CronJob('0 2 * * *', task); // 每天凌晨 2 点
// node-cron: 支持 6 位(秒 分 时 日 月 周)
cron.schedule('0 30 9 * * *', task); // 每天 9:30:00
later 提供最灵活的调度语法,支持自然语言描述和复杂规则组合。
// later: 支持文本解析和自定义约束
const schedule = later.parse.text('every 5 minutes between 9:00 and 17:00');
// 或使用 cron 表达式
const schedule2 = later.parse.cron('0 */2 * * *');
// 可生成未来 5 次执行时间
const times = later.schedule(schedule).next(5);
agenda 内部使用 human-interval 解析自然语言(如 'in 2 hours'),也支持 cron 表达式(通过 agenda.every('*/5 * * * *', ...))。
// agenda: 支持自然语言和 cron
agenda.schedule('tomorrow at 9am', 'task');
agenda.every('*/10 * * * *', 'task'); // 每 10 分钟
✅ 结论:需要复杂调度规则(如“每月最后一个周五”)?选
later。只需标准 cron?cron或node-cron足够。agenda在两者间取得平衡。
cron 的错误处理较弱。若任务抛出异常,整个 Node.js 进程可能崩溃(除非全局捕获)。
// cron: 未捕获异常会导致进程退出
new CronJob('* * * * *', () => {
throw new Error('Oops!'); // 危险!
});
node-cron 默认捕获任务异常并记录,不会中断调度器或其他任务。
// node-cron: 异常被隔离
cron.schedule('* * * * *', () => {
throw new Error('Safe!'); // 仅当前任务失败,调度器继续运行
});
agenda 提供完善的重试机制。任务失败后可自动重试(默认 0 次,可配置),并记录失败原因。
// agenda: 配置重试策略
agenda.define('critical job', { maxConcurrency: 1, retryTimes: 3 }, async (job) => {
await riskyOperation();
});
later 不涉及任务执行,因此无错误处理逻辑 —— 你需要自己包装 try/catch。
✅ 结论:任务可能失败且需自动恢复?选
agenda。只需避免进程崩溃?node-cron更安全。cron适合可控的简单任务。
agenda 本质是一个任务队列系统,支持多进程/多服务器共享任务池(通过同一 MongoDB 实例),天然适合分布式环境。
// 多个服务实例可同时消费同一任务队列
const agenda1 = new Agenda({ db: { address: 'shared-mongo' } });
const agenda2 = new Agenda({ db: { address: 'shared-mongo' } });
// 两者会协调执行任务,避免重复
cron、node-cron 和 later 均为单机调度器。若部署多个实例,每个都会独立触发任务,导致重复执行。
// 在 3 个容器中运行以下代码 → 任务执行 3 次!
cron.schedule('0 0 * * *', () => {
chargeMonthlyFee(); // 危险:可能重复扣费
});
✅ 结论:多实例部署?必须用
agenda(或自行实现分布式锁)。单机应用?其他库均可。
agenda 强依赖 MongoDB,增加基础设施复杂度。cron 和 node-cron 无外部依赖,安装即用。later 无依赖,但需自行实现任务触发和状态跟踪。| 特性 | agenda | cron | later | node-cron |
|---|---|---|---|---|
| 持久化 | ✅ (MongoDB) | ❌ | ❌ | ❌ |
| 分布式支持 | ✅ | ❌ | ❌ | ❌ |
| 时间表达式 | 自然语言 + cron | 标准 cron (5 位) | 文本 + cron + 自定义 | cron (6 位,含秒) |
| 错误隔离 | ✅ (重试机制) | ❌ | ❌ (需自行处理) | ✅ (捕获异常) |
| 外部依赖 | MongoDB | 无 | 无 | 无 |
| 适用场景 | 关键后台作业、分布式系统 | 简单单机脚本 | 调度规则计算引擎 | 健壮的单机定时任务 |
agenda。持久化和分布式能力值得 MongoDB 的开销。node-cron。秒级精度和错误隔离让开发更安心。later 计算时间点,再用 setTimeout 执行。cron。零配置,几行代码搞定。记住:没有“最好”的库,只有“最合适”当前场景的工具。根据任务的关键性、部署环境和维护成本做选择,才能避免过度设计或埋下隐患。
选择 cron 如果你只需要一个轻量级、无依赖的定时器,用于在单个 Node.js 进程中按 cron 表达式执行简单任务。它 API 极简,适合快速原型或小型脚本,但缺乏任务持久化、错误隔离和高级控制(如手动启动/停止之外的动态管理)。当项目对资源敏感且任务失败可接受时,它是合适的选择。
选择 node-cron 如果你需要比 cron 更健壮的错误处理、更清晰的任务生命周期控制(如 start/stop/restart)以及对秒级精度的支持,同时仍希望保持轻量且无需外部数据库。它适合中等复杂度的定时任务场景,例如定期拉取数据、健康检查或缓存刷新,尤其当你需要确保任务异常不会导致整个调度器崩溃时。
选择 agenda 如果你需要一个具备持久化存储、任务重试、优先级管理以及跨进程/服务器协调能力的完整任务队列系统。它依赖 MongoDB 存储任务状态,适合需要高可靠性和容错能力的生产环境,例如发送邮件、数据同步或定期清理等关键后台作业。但如果你不需要持久化或不想引入数据库依赖,它的开销可能过大。
选择 later 如果你的核心需求是解析和计算复杂的调度时间点(例如“每月最后一个工作日”或“每两小时一次但避开午夜”),并希望将调度逻辑与执行逻辑解耦。它本身不运行任务,而是提供时间计算工具,适合集成到自定义调度引擎中。但若你需要开箱即用的任务执行能力,应搭配其他库使用,或考虑其他选项。
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