agenda、cron、later、node-cron はすべて Node.js 環境でタスクをスケジュール実行するためのライブラリですが、それぞれ設計思想や用途が異なります。agenda は MongoDB を使って永続化されたジョブキューを提供し、失敗時の再試行や優先度制御など高度な機能を備えています。一方、cron と node-cron は Unix cron の構文に似た形式で定期実行を定義でき、軽量かつシンプルなユースケースに向いています。later は柔軟なスケジュール記述(例えば「毎月第2火曜日」など)を可能にする一方、開発が停止しており新規プロジェクトでの使用は推奨されません。これらのライブラリは、単純なタイマーから堅牢なジョブ管理システムまで、さまざまなニーズに対応しています。
Node.js で定期実行や遅延実行のタスクを扱う際、いくつかの選択肢があります。agenda、cron、later、node-cron は代表的な npm パッケージですが、それぞれが異なる問題を解決するために作られています。この記事では、実際のコード例を交えながら、各ライブラリの特徴と使いどころを解説します。
later は公式に非推奨(deprecated)となっています。npm ページおよび GitHub リポジトリには「This project is no longer maintained」と明記されており、新規プロジェクトでの使用は避けるべきです。以降の比較では参考のために言及しますが、実用上は他の3つに絞って検討すべきです。
agenda:MongoDB による永続化ジョブキューシステムagenda は単なるタイマーではなく、MongoDB を使ってジョブを永続化する本格的なジョブキューシステムです。プロセスが再起動しても未完了のジョブが失われず、再試行やロック機構も備えています。
// agenda: MongoDB にジョブを保存
const Agenda = require('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();
// 5分後に1回だけ実行
await agenda.schedule('in 5 minutes', 'send email', { to: 'user@example.com' });
cron / node-cron / later:メモリ内スケジューラーこれらはすべて Node.js プロセスのメモリ内で動作し、プロセス終了時にスケジュールが消えます。永続化や再試行機能はありません。
// cron: メモリ内スケジューラー
const CronJob = require('cron').CronJob;
new CronJob('0 */2 * * *', () => {
console.log('2時間ごとに実行');
}, null, true);
// node-cron: 同様にメモリ内
const cron = require('node-cron');
cron.schedule('0 */2 * * *', () => {
console.log('2時間ごとに実行');
});
// later: 非推奨だが参考までに
const later = require('later');
later.date.UTC();
const sched = later.parse.cron('0 */2 * * *');
later.setInterval(() => console.log('2時間ごと'), sched);
cron と node-cron:標準 cron 構文どちらも Unix cron に似た5フィールド構文(分 時 日 月 曜日)を使います。node-cron は秒フィールドをオプションでサポートします。
// cron: 標準5フィールド
new CronJob('0 9 * * 1-5', task); // 平日9時
// node-cron: 秒を含む6フィールドも可
cron.schedule('0 0 9 * * 1-5', task); // 同じく平日9時
later:自然言語に近い柔軟な記述(非推奨)later は「毎月第2火曜日」や「毎週金曜日の15時〜17時」など、非常に複雑なスケジュールを記述できますが、非推奨のためここでは紹介に留めます。
agenda:自然言語または Date オブジェクトagenda は human-interval ライブラリを使って「in 10 minutes」や「at 3:00pm tomorrow」のような自然言語でスケジュールを指定できます。
// agenda: 自然言語によるスケジュール
await agenda.schedule('tomorrow at 3pm', 'backup');
await agenda.schedule('in 2 hours', 'cleanup');
cron:限定的cron はコンストラクタで timeZone オプションを指定できますが、内部的には moment-timezone に依存しており、設定がやや面倒です。
// cron: タイムゾーン指定
new CronJob('0 9 * * *', task, null, true, 'Asia/Tokyo');
node-cron:ネイティブサポートnode-cron は options.timezone で直接タイムゾーンを指定でき、より直感的です。
// node-cron: タイムゾーン指定
cron.schedule('0 9 * * *', task, { timezone: 'Asia/Tokyo' });
agenda:グローバル設定可能agenda はインスタンス作成時に defaultConcurrency や processEvery と共に defaultLockLifetime を設定できますが、タイムゾーンはスケジュール文字列に組み込むか、Date オブジェクトで明示的に指定します。
// agenda: Date オブジェクトでタイムゾーン考慮
const jst = new Date();
jst.setHours(21, 0, 0, 0); // JST 21時
await agenda.schedule(jst, 'nightly-task');
later:手動設定(非推奨)later.date.UTC() や later.date.localTime() で調整可能ですが、非推奨のため詳細は割愛します。
cron:基本的な start/stopCronJob インスタンスには start() と stop() メソッドがありますが、API はやや冗長です。
const job = new CronJob('*/10 * * * * *', task);
job.start();
// ...
job.stop();
node-cron:シンプルな戻り値制御schedule() はスケジュールオブジェクトを返し、そこから start() / stop() が呼び出せます。
const task = cron.schedule('*/10 * * * * *', () => {});
task.start();
// ...
task.stop();
agenda:ジョブの動的管理agenda は cancel()、jobs()、purge() などのメソッドで、実行中のジョブを動的に管理できます。
// 実行中のジョブをキャンセル
await agenda.cancel({ name: 'send email' });
// 特定のジョブを取得
const jobs = await agenda.jobs({ name: 'backup' });
node-croncron よりも直感的なAPI。const cron = require('node-cron');
cron.schedule('* * * * *', checkApi, { timezone: 'UTC' });
agendaawait agenda.schedule('0 2 * * *', 'backup-database');
node-cron + 手動判定later は非推奨のため、cron で毎日実行し、関数内で条件をチェックするのが現実的。// 毎日実行し、最終金曜日かどうかを判定
cron.schedule('0 9 * * *', () => {
if (isLastFriday(new Date())) doTask();
});
| ライブラリ | 永続化 | 再試行 | タイムゾーン | 複雑スケジュール | 非推奨 |
|---|---|---|---|---|---|
agenda | ✅ (MongoDB) | ✅ | ⚠️ (手動) | ⚠️ (自然言語) | ❌ |
cron | ❌ | ❌ | ⚠️ (moment依存) | ❌ | ❌ |
later | ❌ | ❌ | ⚠️ (手動) | ✅ | ✅ |
node-cron | ❌ | ❌ | ✅ | ❌ | ❌ |
node-cron(cron よりも使いやすい)agenda(MongoDB が使える前提)later は絶対に使わない → 非推奨であり、セキュリティリスクや互換性問題の原因になり得るあなたのプロジェクトが「一度失敗したら終わり」でよいのか、「確実に実行されなければならない」のか — その判断が、どのライブラリを選ぶべきかを決めます。
agenda は、MongoDB をバックエンドに持つ堅牢なジョブキューシステムが必要な場合に最適です。ジョブの永続化、再試行、優先度、並列処理制御といった機能が求められる本格的なバッチ処理や非同期タスク管理に適しています。ただし、MongoDB への依存があるため、データベースを使いたくない軽量な用途には不向きです。
cron は、Unix cron 構文に慣れている開発者向けのシンプルなスケジューラーです。軽量で依存が少なく、短時間の定期タスク(例:1分ごとの監視)に適しています。ただし、タイムゾーンサポートが限定的で、複雑なスケジュール(例:「毎月最終金曜日」)には対応していません。
later は非常に柔軟なスケジュール記述が可能でしたが、公式に開発が終了しており、npm ページにも「deprecated」と明記されています。新規プロジェクトでは絶対に使用せず、代わりに cron や node-cron を検討してください。既存プロジェクトで使っている場合は、移行を検討すべきです。
node-cron は cron と同様の cron 構文を使いながら、タイムゾーン指定やより直感的な API を提供します。例えば start() や stop() メソッドでスケジュールの制御が可能です。軽量かつ使いやすく、特にタイムゾーンを考慮した定期実行が必要な場合に cron よりも優れた選択肢です。
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 (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
}
});
// 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) => { /* ... */ });
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