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

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

agendacronlaternode-cronnode-schedule 都是用于在 Node.js 应用中执行定时或周期性任务的库,但它们在持久化能力、调度语法、依赖项和使用场景上有显著差异。agenda 依赖 MongoDB 实现任务持久化和分布式调度;cronnode-cron 基于 Unix cron 表达式提供轻量级内存调度;later 支持更灵活的调度规则(包括 cron、文本描述和自定义时间逻辑);node-schedule 则使用类似 cron 的 RecurrenceRule 对象,支持日期/时间字段的细粒度控制。这些库适用于从简单脚本到高可靠后台作业系统的不同需求。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

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

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

在 Node.js 开发中,定时任务无处不在 —— 从每日数据备份、邮件发送到缓存清理。面对 agendacronlaternode-cronnode-schedule 这五个主流库,如何选择?本文从持久化能力、调度语法、API 设计和适用场景出发,为你提供一份实战导向的技术选型指南。

💾 持久化 vs 内存调度:是否需要任务“断电续跑”?

agenda 是唯一支持任务持久化的库,它将任务存储在 MongoDB 中,即使服务重启,未完成的任务仍能恢复执行。其他四个库(cronlaternode-cronnode-schedule)均只在内存中调度,进程退出即丢失所有任务。

// agenda: 任务持久化示例
const agenda = new Agenda({ db: { address: 'mongodb://localhost:27017/agenda' } });

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

await agenda.start();
agenda.schedule('in 1 hour', 'send email', { to: 'user@example.com' });
// 即使服务重启,1 小时后任务仍会执行
// cron: 内存调度(无持久化)
const job = new CronJob('0 0 * * *', () => {
  console.log('Daily backup');
}, null, true);
// 若进程退出,任务消失
// later: 同样仅内存调度
const schedule = later.parse.text('every 5 mins');
later.setInterval(() => {
  console.log('Check status');
}, schedule);
// node-cron: 内存调度
const job = new CronJob('*/10 * * * * *', async () => {
  await doSomething();
}, null, true);
// node-schedule: 内存调度
const job = schedule.scheduleJob('0 0 12 * * *', () => {
  console.log('Lunch time!');
});

关键结论:若任务不能丢失(如支付对账、通知发送),必须选 agenda;否则可考虑更轻量的内存方案。

📅 调度语法:cron 表达式、自然语言还是对象配置?

不同库对调度规则的定义方式差异显著,直接影响开发体验。

cronnode-cron 仅支持标准 cron 表达式(5 或 6 位),简洁但不够灵活。

later 支持三种方式:cron 表达式、自然语言(如 'every weekday at 9:00')和自定义时间计算函数,适合复杂业务规则。

node-schedule 使用 RecurrenceRule 对象,允许按字段设置(如 rule.hour = 14; rule.minute = 30),便于程序动态生成规则。

agenda 也支持 cron 表达式,但因其持久化特性,通常配合 schedule() 方法使用绝对时间或相对时间字符串。

// cron: 标准 cron 表达式
new CronJob('0 9 * * 1-5', workdayMorningTask); // 工作日上午 9 点
// later: 自然语言 + 程序化组合
const schedule = later.parse.recur()
  .on(9, 17).hour()        // 9点和17点
  .on(later.Days.MON, later.Days.FRI).dayOfWeek(); // 周一和周五
later.setTimeout(task, schedule);
// node-schedule: 对象式规则
const rule = new schedule.RecurrenceRule();
rule.dayOfWeek = [0, new schedule.Range(1, 5)]; // 周日至周五
rule.hour = 10;
rule.minute = 30;
schedule.scheduleJob(rule, task);
// node-cron: 仅支持 cron 字符串
new CronJob('30 10 * * 0-5', task); // 周日至周五上午 10:30
// agenda: 支持 cron 字符串或自然语言时间
agenda.schedule('today at 2pm', 'task');
agenda.schedule('0 14 * * *', 'task');

关键结论:需要动态生成规则?选 node-schedule;规则复杂且含业务语义?选 later;只需标准 cron?cronnode-cron 足够。

⚙️ API 设计与现代 JavaScript 支持

node-cronagenda 提供了最现代的 API,原生支持 async/await。

cronnode-schedule 的回调函数不直接支持 async,需手动处理 Promise(否则错误会被吞掉)。

later 的 API 较底层,更像工具函数集合,需自行管理任务生命周期。

// node-cron: 原生 async 支持
const job = new CronJob('* * * * * *', async () => {
  await fetch('/api/ping');
});
// agenda: 定义任务时直接使用 async
agenda.define('fetch data', async (job) => {
  const data = await fetchData();
  await saveData(data);
});
// cron: 需手动处理 Promise
new CronJob('* * * * * *', () => {
  fetch('/api/ping').catch(console.error); // 必须 catch,否则静默失败
});
// node-schedule: 同样需手动处理
schedule.scheduleJob('* * * * *', () => {
  someAsyncTask().catch(err => {
    logger.error(err);
  });
});
// later: 无内置任务管理
const timer = later.setTimeout(async () => {
  await doWork();
}, schedule);
// 需自行调用 timer.clear() 取消

关键结论:若大量使用 async/await,优先考虑 node-cronagenda;其他库需格外注意错误处理。

🧩 依赖与部署复杂度

agenda 是唯一有外部依赖的库(MongoDB),增加了部署和运维成本。

其余四个库均为纯 JavaScript 实现,零依赖,可直接嵌入任何 Node.js 项目。

// agenda 需要 MongoDB 连接
const agenda = new Agenda({
  db: { address: process.env.MONGO_URI }
});

cronlaternode-cronnode-schedule 安装后即可使用:

npm install cron
# 无需配置数据库或额外服务

关键结论:若项目无 MongoDB 或希望最小化依赖,避开 agenda;否则其持久化能力值得投入。

🛠️ 任务管理与监控能力

agenda 提供完整的任务生命周期管理:可查询、取消、重试任务,并通过 Web UI(如 agenda-ui)监控状态。

其他库仅提供基础的 start/stop/cancel 接口,无内置状态追踪。

// agenda: 查询待执行任务
const jobs = await agenda.jobs({ name: 'send email' });

// 取消特定任务
await agenda.cancel({ name: 'send email' });

node-cron 仅能通过 job.stop() 停止:

const job = new CronJob(...);
job.start();
// 无法查询历史或状态,只能 stop
job.stop();

关键结论:若需审计、重试或可视化任务,agenda 是唯一选择;否则轻量库足够。

📊 总结:如何选择?

场景推荐库理由
高可靠后台作业系统(需持久化、重试、监控)agenda唯一支持 MongoDB 持久化,任务不丢失
简单 cron 任务,无持久化需求node-cron现代 API,支持 async/await,轻量
熟悉传统 cron,追求极简cron社区最广,API 简单直接
复杂调度规则(如“工作日每小时”)later支持自然语言和程序化规则组合
动态生成调度逻辑(如跳过节假日)node-scheduleRecurrenceRule 对象提供细粒度控制

💡 最终建议

  • 新项目且需持久化 → 选 agenda,尽管有 MongoDB 依赖,但换来的是生产级可靠性。
  • 短期脚本或开发工具 → 选 node-croncron,轻量无负担。
  • 规则复杂但无需持久化 → 用 later 的自然语言解析能力提升可读性。
  • 需要程序动态调整调度时间node-schedule 的对象式规则更灵活。

记住:没有“最好”的库,只有“最合适”当前场景的工具。评估你的需求 —— 是否容忍任务丢失?规则是否复杂?是否已有 MongoDB?—— 答案自然浮现。

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

  • agenda:

    选择 agenda 如果你需要任务持久化、失败重试、任务状态追踪或跨多个实例协调定时任务。它基于 MongoDB 存储任务,适合生产环境中对可靠性要求高的后台作业系统,但会引入数据库依赖和额外运维成本。如果你的应用已使用 MongoDB 且需要长期运行的可恢复任务,它是理想选择。

  • cron:

    选择 cron 如果你只需要一个轻量、无依赖的内存级定时器,且熟悉标准 cron 表达式。它不支持任务持久化或跨进程同步,适合单机、短期运行的脚本或开发环境中的简单调度。由于其 API 简洁且社区广泛,是快速实现基础定时功能的首选。

  • later:

    选择 later 如果你需要比 cron 更灵活的调度规则,比如‘每工作日 9 点到 17 点每 30 分钟执行一次’这类复杂逻辑。它支持 cron 表达式、自然语言描述(如 'every 5 mins')和自定义时间计算,但所有调度都在内存中运行,不提供持久化或分布式支持,适合规则复杂但无需高可用的场景。

  • node-cron:

    选择 node-cron 如果你偏好现代 Promise/async 语法且希望使用标准 cron 表达式。它与 cron 功能相似,但提供了更符合现代 JavaScript 习惯的 API(如 start/stop 方法和 async 回调),同样仅限内存调度。适合希望代码简洁、无需外部依赖的中小型项目。

  • node-schedule:

    选择 node-schedule 如果你需要基于 JavaScript 对象(而非字符串表达式)构建调度规则,并希望精确控制年、月、日、时、分等字段。它支持类似 cron 的功能,但通过 RecurrenceRule 对象提供更强的程序化控制能力,例如动态跳过节假日。所有任务仅在内存中运行,适合需要动态生成调度逻辑但不要求持久化的应用。

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