agenda vs cron vs later vs node-cron
Node.js 定时任务调度库选型指南
agendacronlaternode-cron类似的npm包:

Node.js 定时任务调度库选型指南

agendacronlaternode-cron 都是用于在 Node.js 环境中实现定时任务调度的 npm 包,但它们的设计目标、持久化能力、时间表达式语法和适用场景有显著差异。agenda 是一个基于 MongoDB 的持久化任务队列系统,支持任务重试、优先级和分布式调度;cronnode-cron 提供类似 Unix cron 的语法来定义周期性任务,但前者更轻量且仅支持基础功能,后者则提供更丰富的 API 和错误处理机制;later 专注于灵活的时间调度规则,支持文本和 cron 表达式,并能生成未来执行时间列表,但不直接提供任务执行器。这些库适用于从简单脚本到复杂后台作业系统的不同需求。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
agenda09,651295 kB21 个月前MIT
cron08,914161 kB254 个月前MIT
later02,418-9810 年前MIT
node-cron03,245221 kB339 个月前ISC

Node.js 定时任务调度库深度对比:agenda vs cron vs later vs node-cron

在 Node.js 应用中,定时任务无处不在 —— 从每日数据备份到每分钟健康检查。但面对 agendacronlaternode-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();

cronnode-cronlater 均为内存调度器。一旦进程终止,所有计划任务立即消失,无法恢复。

// 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。否则,内存调度器足够。

⏰ 时间表达式灵活性:支持哪些调度规则?

cronnode-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?cronnode-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' } });
// 两者会协调执行任务,避免重复

cronnode-cronlater 均为单机调度器。若部署多个实例,每个都会独立触发任务,导致重复执行。

// 在 3 个容器中运行以下代码 → 任务执行 3 次!
cron.schedule('0 0 * * *', () => {
  chargeMonthlyFee(); // 危险:可能重复扣费
});

结论:多实例部署?必须用 agenda(或自行实现分布式锁)。单机应用?其他库均可。

📦 依赖与运维成本

  • agenda 强依赖 MongoDB,增加基础设施复杂度。
  • cronnode-cron 无外部依赖,安装即用。
  • later 无依赖,但需自行实现任务触发和状态跟踪。

🆚 总结:关键差异速查表

特性agendacronlaternode-cron
持久化✅ (MongoDB)
分布式支持
时间表达式自然语言 + cron标准 cron (5 位)文本 + cron + 自定义cron (6 位,含秒)
错误隔离✅ (重试机制)❌ (需自行处理)✅ (捕获异常)
外部依赖MongoDB
适用场景关键后台作业、分布式系统简单单机脚本调度规则计算引擎健壮的单机定时任务

💡 最终建议

  • 构建 SaaS 后台或微服务? → 用 agenda。持久化和分布式能力值得 MongoDB 的开销。
  • 写一个本地数据抓取脚本? → 用 node-cron。秒级精度和错误隔离让开发更安心。
  • 需要解析“每季度第一个周一”这类规则? → 用 later 计算时间点,再用 setTimeout 执行。
  • 快速验证一个想法? → 用 cron。零配置,几行代码搞定。

记住:没有“最好”的库,只有“最合适”当前场景的工具。根据任务的关键性、部署环境和维护成本做选择,才能避免过度设计或埋下隐患。

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

  • agenda:

    选择 agenda 如果你需要一个具备持久化存储、任务重试、优先级管理以及跨进程/服务器协调能力的完整任务队列系统。它依赖 MongoDB 存储任务状态,适合需要高可靠性和容错能力的生产环境,例如发送邮件、数据同步或定期清理等关键后台作业。但如果你不需要持久化或不想引入数据库依赖,它的开销可能过大。

  • cron:

    选择 cron 如果你只需要一个轻量级、无依赖的定时器,用于在单个 Node.js 进程中按 cron 表达式执行简单任务。它 API 极简,适合快速原型或小型脚本,但缺乏任务持久化、错误隔离和高级控制(如手动启动/停止之外的动态管理)。当项目对资源敏感且任务失败可接受时,它是合适的选择。

  • later:

    选择 later 如果你的核心需求是解析和计算复杂的调度时间点(例如“每月最后一个工作日”或“每两小时一次但避开午夜”),并希望将调度逻辑与执行逻辑解耦。它本身不运行任务,而是提供时间计算工具,适合集成到自定义调度引擎中。但若你需要开箱即用的任务执行能力,应搭配其他库使用,或考虑其他选项。

  • node-cron:

    选择 node-cron 如果你需要比 cron 更健壮的错误处理、更清晰的任务生命周期控制(如 start/stop/restart)以及对秒级精度的支持,同时仍希望保持轻量且无需外部数据库。它适合中等复杂度的定时任务场景,例如定期拉取数据、健康检查或缓存刷新,尤其当你需要确保任务异常不会导致整个调度器崩溃时。

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

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) => { /* ... */ });

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