agenda vs cron vs later vs node-schedule vs scheduler
Node.js 定时任务与作业调度方案深度对比
agendacronlaternode-schedulescheduler类似的npm包:

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

agenda、cron、later、node-schedule 和 scheduler 都是用于在 Node.js 环境中管理定时任务的库,但它们的架构目标和适用场景截然不同。agenda 是一个基于 MongoDB 的完整作业队列系统,支持持久化、分布式执行和重试机制,适合处理关键业务逻辑。cron 和 node-schedule 专注于在单进程内解析 Cron 表达式或自然语言时间来触发函数,轻量但缺乏持久化能力。later 擅长解析复杂的人类可读时间表(如“每周一上午”),常用于前端或轻量后端调度。而 scheduler 通常指代 React 内部的调度器或通用的简易调度工具,在独立的 Node.js 定时任务场景中较少作为首选方案。本文将深入分析这些库在持久化、并发控制、语法灵活性和容错机制上的差异。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
agenda09,706301 kB422 个月前MIT
cron08,951161 kB3710 个月前MIT
later02,405-9811 年前MIT
node-schedule09,20335 kB1734 年前MIT
scheduler0250,73382.7 kB1,38116 天前MIT

Node.js 定时任务架构选型:从轻量 Cron 到分布式作业队列

在 Node.js 后端开发中,定时任务是常见需求,从简单的日志清理到复杂的分布式报表生成。市面上的解决方案大致分为两类:一类是内存级调度器(如 cron, node-schedule),它们轻量但进程重启后任务丢失;另一类是持久化作业队列(如 agenda),它们将任务存入数据库,确保高可靠性和分布式执行。本文将深入对比 agenda、cron、later、node-schedule 和 scheduler,帮助你根据业务场景做出架构决策。

🏗️ 核心架构:内存执行 vs 数据库持久化

这是选择调度库时最重要的分水岭。如果你的任务丢失会导致业务损失(如未发送的账单邮件),必须选择持久化方案。

agenda 基于 MongoDB 存储所有任务定义和执行状态。即使 Node.js 进程崩溃或重启,未完成的任务也不会丢失,且支持多进程竞争锁,防止任务重复执行。

// agenda: 任务定义与持久化
const agenda = new Agenda({ db: { address: 'mongodb://localhost/agenda' } });

agenda.define('send email', async (job) => {
  await sendEmail(job.attrs.data.to);
});

// 任务被存入 MongoDB,即使进程重启也会继续执行
await agenda.every('10 minutes', 'send email', { to: 'user@example.com' });
await agenda.start();

cron、node-schedule 和 later 都将任务保存在内存中。如果进程重启,所有待执行的任务都会丢失。它们适合无状态的清理工作或开发环境测试。

// cron: 纯内存执行
const { CronJob } = require('cron');

const job = new CronJob('*/10 * * * *', () => {
  console.log('Running every 10 minutes');
});
job.start();
// 进程重启后,此任务需代码重新加载才能恢复
// node-schedule: 内存执行,支持更多语法
const schedule = require('node-schedule');

const j = schedule.scheduleJob('*/10 * * * *', () => {
  console.log('The answer to life...');
});
// 同样缺乏持久化,进程挂掉任务即消失

⏱️ 时间表达式:标准 Cron vs 自然语言

不同的库对时间定义的支持程度不同,这直接影响代码的可读性和维护成本。

cron 严格遵循标准的 5 位或 6 位 Cron 表达式。这对运维人员很熟悉,但对非专业人士来说难以阅读(例如 0 0 12 * * ? 代表什么?)。

// cron: 标准 Cron 语法
// 每天中午 12 点执行
const job = new CronJob('0 0 12 * * *', () => {
  console.log('Noon job');
});

node-schedule 不仅支持标准 Cron,还支持人类可读的自然语言字符串(RecurrenceRule),大大降低了理解成本。

// node-schedule: 支持自然语言
const j = schedule.scheduleJob({ hour: 12, minute: 0 }, () => {
  console.log('Noon job - readable syntax');
});

// 或者使用 Rule 对象
const rule = new schedule.RecurrenceRule();
rule.hour = 12;
rule.minute = 0;
schedule.scheduleJob(rule, () => { /*...*/ });

later 的核心优势在于其强大的自然语言解析器,可以处理非常复杂的调度逻辑,如“每周一和周三的上午 9 点到 5 点”。

// later: 极强的自然语言解析
const later = require('later');

// 定义:每周一和周三的上午 9 点到下午 5 点
const sched = later.parse.text('every 15 mins between 9:00 am and 5:00 pm on Mon and Wed');

const timer = later.setInterval(() => {
  console.log('Complex schedule running');
}, sched);

agenda 内部通常使用 cron-parser,因此支持标准 Cron 语法,同时也支持人类可读的字符串(如 'every 10 minutes'),兼顾了灵活性与标准性。

// agenda: 混合支持
// 标准 Cron
await agenda.every('0 0 * * *', 'daily task');

// 自然语言
await agenda.every('10 minutes', 'frequent task');

🛡️ 容错与并发:单机 vs 分布式

在生产环境中,任务执行失败怎么办?多个服务器同时运行会不会重复执行?

agenda 提供了完整的容错机制。它支持任务重试(failCount)、最大重试次数配置,以及基于数据库锁的并发控制。这意味着你可以部署多个 Node.js 实例连接同一个 MongoDB,agenda 会自动确保同一时刻只有一个实例执行特定任务。

// agenda: 配置重试与并发
agenda.define('critical job', async (job) => {
  // 如果失败,agenda 会自动重试
  throw new Error('Temporary failure');
});

// 设置最大重试次数
job.attrs.nextRunAt = new Date();
job.attrs.failCount = 0;
job.attrs.failReason = null;

// 多进程环境下,agenda 自动处理锁,防止重复执行
await agenda.start();

cron、node-schedule 和 later 均无内置重试机制。如果任务代码抛出异常,任务就会终止,除非你在代码内部手动包裹 try-catch 并重新调度。此外,它们在多进程部署时无法协调,会导致每个进程都执行一次任务,造成数据重复。

// cron: 需手动处理错误
const job = new CronJob('* * * * *', () => {
  try {
    doWork();
  } catch (e) {
    console.error('Job failed, no auto-retry');
    // 必须手动实现重试逻辑,否则任务丢失
  }
});

🔄 任务生命周期管理

对于动态任务(如用户自定义的提醒),我们需要能够取消、暂停或修改任务。

node-schedule 提供了非常直观的作业对象管理 API,可以轻松取消或重新调度。

// node-schedule: 灵活的任务管理
const job = schedule.scheduleJob('myJob', '* * * * *', () => {});

// 取消任务
job.cancel();

// 重新调度
job.reschedule('0 0 * * *');

agenda 允许通过查询数据库来查找、取消或修改任务,非常适合动态业务场景。

// agenda: 基于查询的管理
// 取消所有名为 'send email' 且发给特定用户的任务
await agenda.cancel({ name: 'send email', 'data.to': 'user@example.com' });

// 动态创建一次性任务
await agenda.schedule(new Date('2023-12-31 23:59:59'), 'send email', { to: 'user' });

cron 和 later 也支持停止任务,但 API 相对基础,通常只是停止定时器,缺乏复杂的查询管理能力。

// cron: 停止任务
job.stop();

// later: 清除定时器
later.clearInterval(timer);

📦 关于 scheduler 的说明

在 npm 生态中,scheduler 通常指 React 内部使用的调度包(用于并发模式),或者是一些极简的、非生产级的调度工具。它不具备上述库的 Cron 解析、持久化或分布式能力。在构建独立的 Node.js 定时任务系统时,不应将通用的 scheduler 包作为首选,除非你有非常特殊的轻量级需求且清楚其局限性。

📊 总结对比表

特性agendanode-schedulecronlaterscheduler
持久化✅ MongoDB❌ 内存❌ 内存❌ 内存❌ 通常无
分布式支持✅ 自动锁机制❌ 需外部协调❌ 需外部协调❌ 需外部协调❌ 无
时间语法Cron + 自然语言Cron + 自然语言标准 Cron极强自然语言依赖具体实现
自动重试✅ 内置支持❌ 需手动❌ 需手动❌ 需手动❌ 无
适用场景关键业务、邮件、报表单机复杂调度单机简单任务复杂时间规则解析React 内部或极简场景

💡 架构师建议

  1. 关键业务必选 agenda:涉及金钱、通知、数据一致性的任务,必须使用 agenda。它的持久化和分布式锁是其他库无法比拟的,能避免进程重启导致的数据丢失和多实例重复执行问题。
  2. 单机运维选 node-schedule:如果只是单节点服务,且需要灵活的调度语法(如“每天下午 3 点”),node-schedule 比 cron 更易读,API 更丰富。
  3. 复杂时间规则选 later:如果业务逻辑涉及极其复杂的时间窗口(如“每月最后一个工作日的特定时间段”),later 的解析器是最强大的。
  4. 避免在核心业务使用纯内存方案:cron 和 later 虽然轻量,但缺乏容错能力。在生产环境中,务必配合进程守护工具(如 PM2)使用,并意识到重启即丢失任务的风险。

选择正确的调度工具,本质上是在开发便利性与系统可靠性之间做权衡。对于现代微服务架构,agenda 往往是更稳健的长期选择。

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

  • agenda:

    如果你的任务需要高可靠性、持久化存储(防止重启丢失)或在多服务器集群中分布式执行,请选择 agenda。它基于 MongoDB,支持任务重试、优先级队列和锁机制,非常适合发送重要邮件、生成报表等不能失败的业务场景。

  • cron:

    如果你只需要在单个 Node.js 进程中运行简单的周期性任务(如每分钟清理缓存),且不需要任务持久化,cron 是最轻量、最直接的选择。它严格遵循标准的 Cron 语法,适合熟悉的运维开发人员快速上手。

  • later:

    当你需要使用更人性化的自然语言来定义调度时间(例如“每隔 15 分钟”或“工作日早上 9 点”),而不是复杂的 Cron 表达式时,later 是最佳选择。它常用于浏览器端或需要灵活时间解析的轻量级服务中。

  • node-schedule:

    如果你希望在一个库中同时支持标准的 Cron 表达式和人类可读的自然语言时间(如 'every 5 minutes'),并且需要在单进程内管理复杂的作业生命周期(取消、重调度),node-schedule 提供了比 cron 更丰富的 API。

  • scheduler:

    除非你是在 React 应用中利用其并发特性,或者在使用某个特定框架自带的简易调度器,否则在独立的 Node.js 后端定时任务场景中,通常不建议首选名为 scheduler 的通用包。对于生产级任务,应优先考虑具备持久化或更成熟生态的 agenda 或 node-schedule。

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.
InMemoryNotificationChannel❌✅Notifications 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