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。零配置,几行代码搞定。记住:没有“最好”的库,只有“最合适”当前场景的工具。根据任务的关键性、部署环境和维护成本做选择,才能避免过度设计或埋下隐患。
选择 node-cron 如果你需要比 cron 更健壮的错误处理、更清晰的任务生命周期控制(如 start/stop/restart)以及对秒级精度的支持,同时仍希望保持轻量且无需外部数据库。它适合中等复杂度的定时任务场景,例如定期拉取数据、健康检查或缓存刷新,尤其当你需要确保任务异常不会导致整个调度器崩溃时。
选择 agenda 如果你需要一个具备持久化存储、任务重试、优先级管理以及跨进程/服务器协调能力的完整任务队列系统。它依赖 MongoDB 存储任务状态,适合需要高可靠性和容错能力的生产环境,例如发送邮件、数据同步或定期清理等关键后台作业。但如果你不需要持久化或不想引入数据库依赖,它的开销可能过大。
选择 cron 如果你只需要一个轻量级、无依赖的定时器,用于在单个 Node.js 进程中按 cron 表达式执行简单任务。它 API 极简,适合快速原型或小型脚本,但缺乏任务持久化、错误隔离和高级控制(如手动启动/停止之外的动态管理)。当项目对资源敏感且任务失败可接受时,它是合适的选择。
选择 later 如果你的核心需求是解析和计算复杂的调度时间点(例如“每月最后一个工作日”或“每两小时一次但避开午夜”),并希望将调度逻辑与执行逻辑解耦。它本身不运行任务,而是提供时间计算工具,适合集成到自定义调度引擎中。但若你需要开箱即用的任务执行能力,应搭配其他库使用,或考虑其他选项。
Job scheduling for Node.js with overlap prevention, distributed coordination, and background tasks. Schedule recurring tasks with cron expressions, prevent overlapping runs, coordinate across multiple instances, and run heavy jobs in isolated background processes. Zero dependencies, written in TypeScript.
npm install node-cron
import cron from 'node-cron';
cron.schedule('* * * * *', () => {
console.log('running a task every minute');
});
Long-running tasks can overlap when the next tick fires before the previous run finishes. noOverlap skips a run instead of stacking them:
cron.schedule('* * * * *', async () => {
await slowJob();
}, { noOverlap: true });
Running multiple instances of your app? distributed: true ensures only one instance executes each scheduled fire. Out of the box it uses an env-var flag; for high availability, plug in a Redis coordinator:
cron.schedule('0 3 * * *', runNightlyBackup, {
name: 'nightly-backup',
distributed: true,
});
Pass a file path instead of a function to run a job in an isolated forked process, so heavy work never blocks your event loop:
cron.schedule('0 3 * * *', './tasks/backup.js');
Bundlers: background tasks fork a helper that node-cron resolves relative to its own files in
node_modules. If you bundle your app (esbuild, webpack, Rollup, etc.), mark node-cron as external so it stays on disk (--external:node-cron, orexternals: ['node-cron']in webpack). Otherwise the fork fails withCannot find module '.../daemon.js'. Inline function tasks are unaffected.
Every task exposes a single consistent interface for control and inspection:
const task = cron.schedule('0 3 * * *', doWork, {
name: 'nightly-backup',
timezone: 'America/Sao_Paulo',
});
task.stop(); // pause
task.start(); // resume
task.destroy(); // remove permanently
task.getStatus(); // 'stopped' | 'idle' | 'running' | 'destroyed'
task.getNextRun(); // next scheduled Date, or null
task.lastRun(); // { date, result } or { date, error }, or null
Tasks emit lifecycle events for observability:
task.on('execution:finished', (ctx) => console.log('result:', ctx.execution?.result));
task.on('execution:failed', (ctx) => console.error('failed:', ctx.execution?.error));
task.on('execution:overlap', () => console.warn('skipped: previous run still active'));
task.on('execution:skipped', (ctx) => console.log('not elected:', ctx.reason));
task.on('task:failed', () => task.start()); // background task's daemon died unexpectedly (crash, OOM-kill); restart manually
All events: task:started, task:stopped, task:destroyed, task:failed, execution:started, execution:finished, execution:failed, execution:missed, execution:overlap, execution:maxReached, execution:skipped. See Events & Observability.
# ┌────────────── second (optional)
# │ ┌──────────── minute
# │ │ ┌────────── hour
# │ │ │ ┌──────── day of month
# │ │ │ │ ┌────── month
# │ │ │ │ │ ┌──── day of week
# │ │ │ │ │ │
# * * * * * *
| field | value |
|---|---|
| second | 0-59 (optional) |
| minute | 0-59 |
| hour | 0-23 |
| day of month | 1-31 (or L for the last day; L-3 offset from last; 15W, LW for nearest weekday) |
| month | 1-12 (or names) |
| day of week | 0-7 (or names, 0 or 7 are Sunday; 2#3, 5L) |
Supports ranges (1-5), steps (*/2), lists (1,15), named months/weekdays, L (last day of month), L-n (offset from the last day), # (nth weekday), <weekday>L (last weekday of month), W (nearest weekday), and ? (alias for * in the day fields, for Quartz-style expressions). See the Cron Syntax guide.
An inverted range wraps around the field instead of being rejected: 22-2 in the hour field means 22:00 through 02:59 (22,23,0,1,2), and sat-sun in the day-of-week field means saturday,sunday.
The W modifier in the day-of-month field fires on the nearest weekday (Monday-Friday) to a given day, without crossing the month boundary: 15W is the nearest weekday to the 15th, 1W the first weekday of the month, and LW the last weekday of the month. Only weekends are adjusted for; there is no holiday awareness.
The L-n form fires n days before the last day of the month (L-3 is the third-to-last day). In months where the offset reaches before the 1st (e.g. L-29 in February), it simply does not fire that month.
Note on Quartz:
L,L-n,W,LW,#,<weekday>Land?are borrowed from Quartz, but node-cron is not Quartz-compatible. Two differences matter:
- Day-of-week numbering is standard cron, not Quartz:
0-7with0/7= Sunday and1= Monday. In Quartz1= Sunday, so the same numeric weekday fires on a different day.- day-of-month and day-of-week are combined with AND (both must match), and may both be set; Quartz instead treats them as mutually exclusive and requires
?in one of them.
?is accepted purely as an alias for*in the day fields so Quartz-style expressions parse, not as a semantic compatibility guarantee.
cron.schedule('0 3 * * *', task, {
name: 'nightly-backup',
timezone: 'America/Sao_Paulo',
noOverlap: true,
distributed: true,
maxExecutions: 10,
maxRandomDelay: 30000,
});
See Scheduling Options for the full list.
Schedules match wall-clock time in the task's timezone. Across a daylight-saving fall-back the repeated hour runs once, so a sub-hourly schedule (for example */15) can pause for up to the length of the DST shift during that hour. If you need a fixed interval to keep firing uninterrupted across DST transitions, use a zone without DST, for example timezone: 'UTC'. See Timezones & DST for the full model.
v4 is a TypeScript rewrite with a smarter scheduler and a streamlined API. See the Migration Guide.
node-cron is zero-dependency infrastructure used in production by 220,000+ repositories. If it is part of your stack, sponsoring helps keep it tested, DST-correct, and maintained.
Become a sponsor on GitHub Sponsors or Open Collective.
Feel free to submit issues and enhancement requests here.
In general, we follow the "fork-and-pull" Git workflow.
NOTE: Be sure to merge the latest from "upstream" before making a pull request!
Please do not contribute code you did not write yourself, unless you are certain you have the legal ability to do so. Also ensure all contributed code can be distributed under the ISC License.
node-cron is under ISC License.