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。零配置,几行代码搞定。记住:没有“最好”的库,只有“最合适”当前场景的工具。根据任务的关键性、部署环境和维护成本做选择,才能避免过度设计或埋下隐患。
选择 agenda 如果你需要一个具备持久化存储、任务重试、优先级管理以及跨进程/服务器协调能力的完整任务队列系统。它依赖 MongoDB 存储任务状态,适合需要高可靠性和容错能力的生产环境,例如发送邮件、数据同步或定期清理等关键后台作业。但如果你不需要持久化或不想引入数据库依赖,它的开销可能过大。
选择 cron 如果你只需要一个轻量级、无依赖的定时器,用于在单个 Node.js 进程中按 cron 表达式执行简单任务。它 API 极简,适合快速原型或小型脚本,但缺乏任务持久化、错误隔离和高级控制(如手动启动/停止之外的动态管理)。当项目对资源敏感且任务失败可接受时,它是合适的选择。
选择 later 如果你的核心需求是解析和计算复杂的调度时间点(例如“每月最后一个工作日”或“每两小时一次但避开午夜”),并希望将调度逻辑与执行逻辑解耦。它本身不运行任务,而是提供时间计算工具,适合集成到自定义调度引擎中。但若你需要开箱即用的任务执行能力,应搭配其他库使用,或考虑其他选项。
选择 node-cron 如果你需要比 cron 更健壮的错误处理、更清晰的任务生命周期控制(如 start/stop/restart)以及对秒级精度的支持,同时仍希望保持轻量且无需外部数据库。它适合中等复杂度的定时任务场景,例如定期拉取数据、健康检查或缓存刷新,尤其当你需要确保任务异常不会导致整个调度器崩溃时。
A light-weight job scheduling library for Node.js
Migrating from v5? See the Migration Guide for all breaking changes.
Agenda 6.x is a complete TypeScript rewrite with a focus on modularity and flexibility:
Pluggable storage backends - Choose from MongoDB, PostgreSQL, Redis, or implement your own. Each backend is a separate package - install only what you need.
Pluggable notification channels - Move beyond polling with real-time job notifications via Redis, PostgreSQL LISTEN/NOTIFY, or other pub/sub systems. Jobs get processed immediately when saved, not on the next poll cycle.
Modern stack - ESM-only, Node.js 18+, full TypeScript with strict typing.
See the 6.x Roadmap for details and progress.
Install the core package and your preferred backend:
# For MongoDB
npm install agenda @agendajs/mongo-backend
# For PostgreSQL
npm install agenda @agendajs/postgres-backend
# For Redis
npm install agenda @agendajs/redis-backend
Requirements:
import { Agenda } from 'agenda';
import { MongoBackend } from '@agendajs/mongo-backend';
const agenda = new Agenda({
backend: new MongoBackend({ address: 'mongodb://localhost/agenda' })
});
// Define a job
agenda.define('send email', async (job) => {
const { to, subject } = job.attrs.data;
await sendEmail(to, subject);
});
// Start processing
await agenda.start();
// Schedule jobs
await agenda.every('1 hour', 'send email', { to: 'user@example.com', subject: 'Hello' });
await agenda.schedule('in 5 minutes', 'send email', { to: 'admin@example.com', subject: 'Report' });
await agenda.now('send email', { to: 'support@example.com', subject: 'Urgent' });
| Package | Backend | Notifications | Install |
|---|---|---|---|
@agendajs/mongo-backend | MongoDB | Polling only | npm install @agendajs/mongo-backend |
@agendajs/postgres-backend | PostgreSQL | LISTEN/NOTIFY | npm install @agendajs/postgres-backend |
@agendajs/redis-backend | Redis | Pub/Sub | npm install @agendajs/redis-backend |
| Backend | Storage | Notifications | Notes |
|---|---|---|---|
MongoDB (MongoBackend) | ✅ | ❌ | Storage only. Combine with external notification channel for real-time. |
PostgreSQL (PostgresBackend) | ✅ | ✅ | Full backend. Uses LISTEN/NOTIFY for notifications. |
Redis (RedisBackend) | ✅ | ✅ | Full backend. Uses Pub/Sub for notifications. |
| InMemoryNotificationChannel | ❌ | ✅ | Notifications only. For single-process/testing. |
import { Agenda } from 'agenda';
import { MongoBackend } from '@agendajs/mongo-backend';
// Via connection string
const agenda = new Agenda({
backend: new MongoBackend({ address: 'mongodb://localhost/agenda' })
});
// Via existing MongoDB connection
const agenda = new Agenda({
backend: new MongoBackend({ mongo: existingDb })
});
// With options
const agenda = new Agenda({
backend: new MongoBackend({
mongo: db,
collection: 'jobs' // Collection name (default: 'agendaJobs')
}),
processEvery: '30 seconds', // Job polling interval
maxConcurrency: 20, // Max concurrent jobs
defaultConcurrency: 5 // Default per job type
});
import { Agenda } from 'agenda';
import { PostgresBackend } from '@agendajs/postgres-backend';
const agenda = new Agenda({
backend: new PostgresBackend({
connectionString: 'postgresql://user:pass@localhost:5432/mydb'
})
});
import { Agenda } from 'agenda';
import { RedisBackend } from '@agendajs/redis-backend';
const agenda = new Agenda({
backend: new RedisBackend({
connectionString: 'redis://localhost:6379'
})
});
For faster job processing across distributed systems:
import { Agenda, InMemoryNotificationChannel } from 'agenda';
import { MongoBackend } from '@agendajs/mongo-backend';
const agenda = new Agenda({
backend: new MongoBackend({ mongo: db }),
notificationChannel: new InMemoryNotificationChannel()
});
You can use MongoDB for storage while using a different system for real-time notifications:
import { Agenda } from 'agenda';
import { MongoBackend } from '@agendajs/mongo-backend';
import { RedisBackend } from '@agendajs/redis-backend';
// MongoDB for storage + Redis for real-time notifications
const redisBackend = new RedisBackend({ connectionString: 'redis://localhost:6379' });
const agenda = new Agenda({
backend: new MongoBackend({ mongo: db }),
notificationChannel: redisBackend.notificationChannel
});
This is useful when you want MongoDB's proven durability and flexible queries for job storage, but need faster real-time notifications across multiple processes.
// Simple async handler
agenda.define('my-job', async (job) => {
console.log('Processing:', job.attrs.data);
});
// With options
agenda.define('my-job', async (job) => { /* ... */ }, {
concurrency: 10,
lockLimit: 5,
lockLifetime: 10 * 60 * 1000, // 10 minutes
priority: 'high'
});
For a class-based approach, use TypeScript decorators:
import { JobsController, Define, Every, registerJobs, Job } from 'agenda';
@JobsController({ namespace: 'email' })
class EmailJobs {
@Define({ concurrency: 5 })
async sendWelcome(job: Job<{ userId: string }>) {
console.log('Sending welcome to:', job.attrs.data.userId);
}
@Every('1 hour')
async cleanupBounced(job: Job) {
console.log('Cleaning up bounced emails');
}
}
registerJobs(agenda, [new EmailJobs()]);
await agenda.start();
// Schedule using namespaced name
await agenda.now('email.sendWelcome', { userId: '123' });
See Decorators Documentation for full details.
// Run immediately
await agenda.now('my-job', { userId: '123' });
// Run at specific time
await agenda.schedule('tomorrow at noon', 'my-job', data);
await agenda.schedule(new Date('2024-12-25'), 'my-job', data);
// Run repeatedly
await agenda.every('5 minutes', 'my-job');
await agenda.every('0 * * * *', 'my-job'); // Cron syntax
// Cancel jobs (removes from database)
await agenda.cancel({ name: 'my-job' });
// Disable/enable jobs globally (by query)
await agenda.disable({ name: 'my-job' }); // Disable all jobs matching query
await agenda.enable({ name: 'my-job' }); // Enable all jobs matching query
// Disable/enable individual jobs
const job = await agenda.create('my-job', data);
job.disable();
await job.save();
// Progress tracking
agenda.define('long-job', async (job) => {
for (let i = 0; i <= 100; i += 10) {
await doWork();
await job.touch(i); // Report progress 0-100
}
});
// Stop immediately - unlocks running jobs so other workers can pick them up
await agenda.stop();
// Drain - waits for running jobs to complete before stopping
await agenda.drain();
// Drain with timeout (30 seconds) - for cloud platforms with shutdown deadlines
const result = await agenda.drain(30000);
if (result.timedOut) {
console.log(`${result.running} jobs still running after timeout`);
}
// Drain with AbortSignal - for external control
const controller = new AbortController();
setTimeout(() => controller.abort(), 30000);
await agenda.drain({ signal: controller.signal });
Use drain() for graceful shutdowns where you want in-progress jobs to finish their work.
agenda.on('start', (job) => console.log('Job started:', job.attrs.name));
agenda.on('complete', (job) => console.log('Job completed:', job.attrs.name));
agenda.on('success', (job) => console.log('Job succeeded:', job.attrs.name));
agenda.on('fail', (err, job) => console.log('Job failed:', job.attrs.name, err));
// Job-specific events
agenda.on('start:send email', (job) => { /* ... */ });
agenda.on('fail:send email', (err, job) => { /* ... */ });
For databases other than MongoDB, PostgreSQL, or Redis, implement AgendaBackend:
import { AgendaBackend, JobRepository } from 'agenda';
class SQLiteBackend implements AgendaBackend {
readonly repository: JobRepository;
readonly notificationChannel = undefined; // Or implement NotificationChannel
async connect() { /* ... */ }
async disconnect() { /* ... */ }
}
const agenda = new Agenda({
backend: new SQLiteBackend({ path: './jobs.db' })
});
See Custom Backend Driver for details.
Official Backend Packages:
Tools:
MIT