agenda vs bree vs bull vs cron vs later vs node-cron vs node-schedule
Node.js 定时任务与作业调度库深度对比
agendabreebullcronlaternode-cronnode-schedule类似的npm包:

Node.js 定时任务与作业调度库深度对比

agendabreebullcronlaternode-cronnode-schedule 都是用于在 Node.js 环境中执行定时任务或延迟作业的库,但它们的架构理念、依赖环境和适用场景截然不同。cronnode-cronnode-schedule 专注于基于 Cron 表达式的内存级定时触发,适合简单的周期性任务;later 提供更灵活的自然语言式调度逻辑但已停止维护;agendabull 是基于数据库(MongoDB 和 Redis)的重型作业队列,支持持久化、重试和分布式处理;bree 则是一个现代化的轻量级包装器,旨在简化原生 worker_threads 和 Cron 的使用,强调隔离性和易用性。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
agenda09,691301 kB4916 天前MIT
bree03,29092 kB296 个月前MIT
bull016,247309 kB1472 年前MIT
cron08,947161 kB448 个月前MIT
later02,406-9811 年前MIT
node-cron03,271328 kB41 个月前ISC
node-schedule09,20735 kB1734 年前MIT

Node.js 定时任务与作业调度库深度对比:从内存 Cron 到分布式队列

在 Node.js 生态中,处理定时任务(Scheduled Jobs)和背景作业(Background Jobs)有多种方案。开发者常面临的选择困境是:究竟该用简单的内存定时器,还是引入沉重的数据库依赖?本文将深入对比 agendabreebullcronlaternode-cronnode-schedule,帮助你根据实际工程需求做出架构决策。

🕒 核心机制:内存计时 vs 持久化队列

这是选择库时的第一个分水岭。cronnode-cronnode-schedulelater 将调度信息保存在内存中。这意味着如果 Node.js 进程重启,所有待执行的任务都会丢失。相反,agendabull 将任务存储在数据库(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();

🧵 执行模型:阻塞主线程 vs 线程隔离

在单线程的 Node.js 中,长时间运行的任务会阻塞事件循环,导致 API 响应变慢。传统的 node-cronnode-schedule 默认在主线程执行回调。而 breebull 提供了更好的隔离方案。

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 vs 灵活规则

对于调度频率的定义,不同库提供了不同粒度的控制。

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);

breebull 均支持标准的 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 组合的最佳实践方案。

🌐 真实场景决策指南

场景 1:简单的日志清理或健康检查

需求:每天凌晨运行一次,无需持久化,代码简单。

  • 推荐: node-cronbree
  • 理由:轻量级,零配置。bree 更能防止阻塞主线程。
// 使用 node-cron
cron.schedule('0 0 * * *', () => cleanupLogs());

场景 2:发送营销邮件或生成大型报表

需求:任务耗时久,不能阻塞 API 响应,失败需重试。

  • 推荐: bullagenda
  • 理由:需要持久化保证不丢失,需要重试机制,需要独立进程处理耗时逻辑。
// 使用 bull
queue.add('generate-report', { userId: 123 }, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } });

场景 3:复杂的动态调度系统

需求:用户可在界面动态设置任意时间的提醒,支持单次和循环。

  • 推荐: node-schedule (小规模) 或 agenda (大规模)
  • 理由node-schedule 支持 Date 对象,适合动态添加一次性任务。若需集群支持,则选 agenda
// 使用 node-schedule 动态添加
schedule.scheduleJob(userSelectedDate, () => sendReminder());

📊 总结对比表

特性node-cronnode-schedulebreebullagendalater
存储方式内存内存配置/内存RedisMongoDB内存
进程重启存活✅ (配置持久化)
执行隔离主线程主线程Worker Threads独立 Worker主线程/Worker主线程
调度语法CronCron + Date + RuleCron + IntervalCron + IntervalCron + HumanNatural Language
重试机制✅ (基础)✅ (高级)✅ (高级)
分布式支持
维护状态❌ (弃用)

💡 架构师建议

对于大多数现代 Node.js 应用,bree 是最平衡的选择:它既解决了主线程阻塞问题,又提供了简单的 Cron 支持,且没有重型数据库依赖。

如果你的业务涉及关键数据流(如支付回调、重要通知),必须保证任务绝对不丢失且支持横向扩展,那么 bull (配合 Redis) 是工业界的标准答案。

尽量避免使用 later,并在需要复杂调度逻辑时优先考虑 node-schedule 而非原始的 cron 包。记住,简单的工具解决简单的问题,复杂的工具解决可靠性的问题 —— 不要为了杀鸡而用牛刀,也不要为了盖楼而只用积木。

如何选择: agenda vs bree vs bull vs cron vs later vs node-cron vs node-schedule

  • agenda:

    如果你的项目已经使用 MongoDB 且需要强大的作业持久化、重试机制和分布式处理能力,选择 agenda。它适合需要确保任务不丢失、支持复杂查询筛选作业以及需要管理后台任务队列的企业级应用,但需注意其依赖 MongoDB 带来的运维成本。

  • bree:

    选择 bree 如果你希望以最小的配置快速启动定时任务,同时利用 Node.js 原生的 worker_threads 实现任务隔离以防止主线程阻塞。它非常适合现代 Node.js 项目,特别是那些希望避免重型依赖(如 Redis/MongoDB)且需要简单 Cron 调度或脚本定期执行的场景。

  • bull:

    当你的系统需要高吞吐量、极高的可靠性以及复杂的作业流控制(如优先级、延迟、重复、沙箱处理)时,bull 是最佳选择。基于 Redis 的特性使其在分布式环境和微服务架构中表现卓越,适合处理关键业务逻辑的背景作业,但需承担 Redis 的维护成本。

  • cron:

    仅在对性能有极致要求且不需要人类可读的 Cron 表达式时选择基础的 cron 包。它提供了最底层的 Cron 解析能力,通常作为其他库的底层依赖,对于大多数应用层开发来说,直接使用更上层的封装(如 node-cron)通常是更好的选择。

  • later:

    避免在新项目中使用 later,因为它已不再积极维护。虽然它曾提供比标准 Cron 更灵活的调度语法(如自然语言解析),但在现代 Node.js 生态中,其功能已被 node-schedule 或更专业的作业队列库所取代,继续使用可能带来兼容性风险。

  • node-cron:

    如果你需要一个零依赖、轻量级且仅用于在单进程中执行简单周期性任务的库,node-cron 是理想选择。它完美兼容标准 Cron 语法,适合日志轮转、健康检查等不需要持久化或分布式的简单场景,但不适合长运行或关键任务。

  • node-schedule:

    选择 node-schedule 如果你需要比标准 Cron 更丰富的调度规则(如支持 Date 对象、递归规则)且希望保持代码简洁。它在内存中运行,适合中等复杂度的定时任务,但在进程重启后会丢失任务状态,因此不适用于需要严格保证执行的关键作业。

agenda的README

Agenda

Agenda

A light-weight job scheduling library for Node.js

NPM Version NPM Downloads

Migrating from v5? See the Migration Guide for all breaking changes.

Agenda 6.x

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.

Features

  • Minimal overhead job scheduling
  • Pluggable storage backends (MongoDB, PostgreSQL, Redis)
  • TypeScript support with full typing
  • Scheduling via cron or human-readable syntax
  • Configurable concurrency and locking
  • Real-time job notifications (optional)
  • Sandboxed worker execution via fork mode
  • TypeScript decorators for class-based job definitions

Installation

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:

  • Node.js 18+
  • Database of your choice (MongoDB 4+, PostgreSQL, or Redis)

Quick Start

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' });

Official Backend Packages

PackageBackendNotificationsInstall
@agendajs/mongo-backendMongoDBPolling onlynpm install @agendajs/mongo-backend
@agendajs/postgres-backendPostgreSQLLISTEN/NOTIFYnpm install @agendajs/postgres-backend
@agendajs/redis-backendRedisPub/Subnpm install @agendajs/redis-backend

Backend Capabilities

BackendStorageNotificationsNotes
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.
InMemoryNotificationChannelNotifications only. For single-process/testing.

Backend Configuration

MongoDB

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
});

PostgreSQL

import { Agenda } from 'agenda';
import { PostgresBackend } from '@agendajs/postgres-backend';

const agenda = new Agenda({
  backend: new PostgresBackend({
    connectionString: 'postgresql://user:pass@localhost:5432/mydb'
  })
});

Redis

import { Agenda } from 'agenda';
import { RedisBackend } from '@agendajs/redis-backend';

const agenda = new Agenda({
  backend: new RedisBackend({
    connectionString: 'redis://localhost:6379'
  })
});

Real-Time Notifications

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()
});

Mixing Storage and Notification Backends

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.

API Overview

Defining Jobs

// 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'
});

Defining Jobs with Decorators

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.

Scheduling Jobs

// 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

Job Control

// 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
  }
});

Stopping / Draining

// 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.

Events

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
	});
});

Custom Backend

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.

Documentation

Related Packages

Official Backend Packages:

Tools:

License

MIT