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 包。记住,简单的工具解决简单的问题,复杂的工具解决可靠性的问题 —— 不要为了杀鸡而用牛刀,也不要为了盖楼而只用积木。
如果你的项目已经使用 MongoDB 且需要强大的作业持久化、重试机制和分布式处理能力,选择 agenda。它适合需要确保任务不丢失、支持复杂查询筛选作业以及需要管理后台任务队列的企业级应用,但需注意其依赖 MongoDB 带来的运维成本。
选择 bree 如果你希望以最小的配置快速启动定时任务,同时利用 Node.js 原生的 worker_threads 实现任务隔离以防止主线程阻塞。它非常适合现代 Node.js 项目,特别是那些希望避免重型依赖(如 Redis/MongoDB)且需要简单 Cron 调度或脚本定期执行的场景。
当你的系统需要高吞吐量、极高的可靠性以及复杂的作业流控制(如优先级、延迟、重复、沙箱处理)时,bull 是最佳选择。基于 Redis 的特性使其在分布式环境和微服务架构中表现卓越,适合处理关键业务逻辑的背景作业,但需承担 Redis 的维护成本。
仅在对性能有极致要求且不需要人类可读的 Cron 表达式时选择基础的 cron 包。它提供了最底层的 Cron 解析能力,通常作为其他库的底层依赖,对于大多数应用层开发来说,直接使用更上层的封装(如 node-cron)通常是更好的选择。
避免在新项目中使用 later,因为它已不再积极维护。虽然它曾提供比标准 Cron 更灵活的调度语法(如自然语言解析),但在现代 Node.js 生态中,其功能已被 node-schedule 或更专业的作业队列库所取代,继续使用可能带来兼容性风险。
如果你需要一个零依赖、轻量级且仅用于在单进程中执行简单周期性任务的库,node-cron 是理想选择。它完美兼容标准 Cron 语法,适合日志轮转、健康检查等不需要持久化或分布式的简单场景,但不适合长运行或关键任务。
选择 node-schedule 如果你需要比标准 Cron 更丰富的调度规则(如支持 Date 对象、递归规则)且希望保持代码简洁。它在内存中运行,适合中等复杂度的定时任务,但在进程重启后会丢失任务状态,因此不适用于需要严格保证执行的关键作业。
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 matching a filter (removes from database)
await agenda.cancel({ name: 'my-job' });
await agenda.cancel({ name: 'my-job', data: { userId: 123 } });
// Cancel ALL jobs unconditionally
await agenda.cancelAll();
// 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) => { /* ... */ });
Use fail listeners to capture richer error context, such as stack traces,
without storing large payloads in job.attrs.failReason:
agenda.on('fail', async (err, job) => {
await saveJobError({
jobId: job.attrs._id,
jobName: job.attrs.name,
message: err.message,
stack: err.stack
});
});
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