これらのライブラリは、Node.js アプリケーションでメール送信、画像処理、データ集計などの重いタスクをバックグラウンドで非同期に実行するためのツールです。メインのリクエスト処理をブロックせずに済むため、ユーザー体験の向上やシステム全体の安定性に貢献します。Redis や MongoDB などの外部ストレージを利用してジョブを管理し、失敗時のリトライやスケジュール実行といった機能を提供します。
Node.js でバックグラウンドタスクを処理する際、どのライブラリを選ぶかはシステムの信頼性に直結します。agenda、bee-queue、bree、bull、kue、node-resque はすべてジョブキューを管理しますが、内部の仕組みや適した場面が異なります。実務で直面する課題に基づき、主要な違いを解説します。
ジョブの状態をどこに保存するかは、インフラ構成に大きな影響を与えます。
agenda は MongoDB を使用します。
// agenda: MongoDB 接続設定
const agenda = new Agenda({ db: { address: 'mongodb://localhost/agenda' } });
await agenda.start();
bee-queue、bull、kue、node-resque は Redis を使用します。
// bull: Redis 接続設定
const Queue = require('bull');
const queue = new Queue('my-queue', 'redis://127.0.0.1:6379');
bree はファイルシステムとプロセス管理を重視します。
// bree: 設定ファイルベース
const Bree = require('bree');
const bree = new Bree({
root: './jobs',
jobs: [{ name: 'daily-report', interval: 'daily' }]
});
ジョブをどのように登録し、実行するかは開発体験に直結します。
agenda は名前付きジョブを定義します。
// agenda: ジョブ定義と実行
agenda.define('send email', async (job) => {
console.log(`Sending to ${job.data.to}`);
});
await agenda.schedule('in 5 minutes', 'send email', { to: '[email protected]' });
bee-queue はキューインスタンスに対して処理を登録します。
// bee-queue: 処理登録
queue.process(async (job) => {
console.log(`Processing job ${job.id}`);
return job.data;
});
const job = queue.createJob({ id: 123 });
await job.save();
bull もキューベースの処理登録を行います。
queue.process で処理を定義し、queue.add でジョブを投入します。// bull: 処理登録と追加
queue.process(async (job) => {
return doSomething(job.data);
});
await queue.add({ foo: 'bar' });
bree は独立したファイルとしてジョブを記述します。
./jobs/daily-report.js のようなファイルを作成し、そこでロジックを書きます。// bree: ワーカースクリプト (jobs/daily-report.js)
module.exports = async () => {
console.log('Running daily report');
};
kue はチェーンメソッドでジョブを作成します。
// kue: ジョブ作成
const job = queue.create('email', { to: 'user' });
job.save((err) => {
if (!err) console.log('Job saved');
});
node-resque はクラスベースでキューとワーカーを管理します。
Queue クラスで enqueue し、Worker クラスで処理します。// node-resque: キューとワーカー
const { Worker, Queue } = require('node-resque');
const queue = new Queue({ connection: { host: 'localhost' } });
await queue.connect();
await queue.enqueue('queueName', 'jobName', ['arg1']);
特定の時刻にジョブを実行したい場合、ライブラリの対応状況が重要です。
agenda はスケジュール機能が強力です。
schedule や every メソッドで、人間が読める形式の時間を指定できます。// agenda: スケジュール実行
await agenda.every('10 minutes', 'calculate stats');
await agenda.schedule('tomorrow at noon', 'send report');
bree は cron 構文や間隔指定をサポートします。
interval や cron を書くだけで済みます。// bree: 間隔指定
const bree = new Bree({
jobs: [
{ name: 'cleanup', interval: '5m' },
{ name: 'backup', cron: '0 0 * * *' }
]
});
bull はジョブ追加時にオプションで指定します。
repeat オプションを使って、ジョブの繰り返しを設定します。// bull: 繰り返しジョブ
await queue.add('send notification', {}, {
repeat: { cron: '*/5 * * * *' }
});
bee-queue、kue、node-resque は基本的なキュー機能に留まります。
// node-resque: 基本的な enqueue のみ
// スケジュール機能は本体にないため、外部で管理が必要
await queue.enqueue('high', 'sendEmail', ['[email protected]']);
ライブラリの保守状況は、長期的なプロジェクトの安定性を決めます。
kue は公式に非推奨です。
// kue: 使用を避けるべき
// const kue = require('kue'); // 非推奨
bull はメンテナンスモードに入っています。
bullmq で開発されています。bullmq の検討を推奨しますが、bull もまだ広く使われています。// bull: 後継に注意
// const Queue = require('bull'); // _legacy_
// const { Queue } = require('bullmq'); // _modern_
agenda、bee-queue、bree、node-resque は継続してメンテナンスされています。
// bree: 活発なメンテナンス
const Bree = require('bree'); // 最新版が利用可能
ジョブが失敗したときにどう振る舞うかは、システムの信頼性に影響します。
agenda は再試行ロジックを内蔵しています。
// agenda: 再試行設定
agenda.define('task', { priority: 'high', concurrency: 10 }, async (job) => {
// 失敗時は自動的にリトライ設定に従う
});
bull は詳細なアトンプト設定を持っています。
attempts オプションで再試行回数を指定できます。// bull: アトンプト設定
queue.add('video process', { id: 1 }, {
attempts: 3,
backoff: { type: 'fixed', delay: 2000 }
});
bee-queue はシンプルにエラーを返します。
// bee-queue: エラー処理
queue.process(async (job) => {
if (!job.data.valid) throw new Error('Invalid data');
});
bree はワーカープロセスの再起動で対応します。
// bree: ワーカー管理
bree.on('worker error', (name, err) => {
console.error(`Worker ${name} failed`, err);
// 必要に応じて手動で再起動トリガー
});
agenda は MongoDB を使っているチームに最適です — データベース統合がスムーズで、スケジュール機能が豊富です。
bull は Redis 環境で高機能なキューが必要な場合に適していますが、bullmq への移行を視野に入れるべきです — 機能は豊富ですが、レガシーになりつつあります。
bree は cron ジョブや定期タスクに特化しています — 別プロセス実行による安定性が魅力で、設定も簡単です。
bee-queue はシンプルさを求める場合に選びます — Redis を使いつつ、設定を最小限に抑えたいプロジェクトに向いています。
kue は使用してはいけません — 非推奨であり、セキュリティリスクがあります。
node-resque は Resque エコシステムとの互換性が必要な場合に意味があります — 特定の環境以外では、他のモダンな選択肢を検討すべきです。
最終的には、既存のインフラ(Redis か MongoDB か)と、求められる機能(単純なキューか、複雑なスケジュールか)で決定します。
Redis ベースで堅牢な機能セットが必要な場合に適していますが、後継の bullmq への移行も検討すべきです。ジョブの優先順位やイベント処理など、エンタープライズ向けの機能が充実しています。大規模な分散処理システムで実績が多いライブラリです。
MongoDB を既に使用しているプロジェクトで、ジョブの永続性と柔軟なスケジュール管理を重視する場合に選びます。データベースにジョブ状態を保存するため、Redis の導入が不要な点がメリットです。複雑な再試行ロジックや頻繁なスケジューリングが必要なシステムに適しています。
Redis を使ったシンプルで軽量なキュー構成が必要な場合に適しています。設定項目が少なく、コードが直感的であるため、小規模から中規模のタスク処理に最適です。高度な機能よりも、導入の手軽さと動作の確実性を優先するプロジェクトで選ばれます。
cron ジョブや定期的なタスク実行をメインの目的とする場合に選びます。他のキューライブラリとは異なり、ワーカーを別プロセスとして実行するため、メモリリークのリスクを隔離できます。Node.js スクリプトを直接ジョブとして登録できるため、開発者の負担が少ないのが特徴です。
新規プロジェクトでは使用しないでください。このライブラリは公式に非推奨(Deprecated)となっており、メンテナンスが停止しています。セキュリティリスクやバグ修正が受けられないため、既存システムからの移行を強く推奨します。
Ruby の Resque システムと互換性が必要な場合や、Resque プロトコルに基づく既存インフラと連携する場合に選びます。Redis をバックエンドに使用し、シンプルな FIFO キュー構成を提供します。特定のレガシーシステムとの統合が必要なシーンで価値を発揮します。
The fastest, most reliable, Redis-based queue for Node.
Carefully written for rock solid stability and atomicity.
Sponsors · Features · UIs · Install · Quick Guide · Documentation
Check the new Guide!
|
| Dragonfly is a new Redis™ drop-in replacement that is fully compatible with BullMQ and brings some important advantages over Redis™ such as massive better performance by utilizing all CPU cores available and faster and more memory efficient data structures. Read more here on how to use it with BullMQ. |
Bull is currently in maintenance mode, we are only fixing bugs. For new features check BullMQ, a modern rewritten implementation in Typescript. You are still very welcome to use Bull if it suits your needs, which is a safe, battle tested library.
Follow me on Twitter for other important news and updates.
You can find tutorials and news in this blog: https://blog.taskforce.sh/
Bull is popular among large and small organizations, like the following ones:
|
|
|
|
|
Supercharge your queues with a professional front end:
Sign up at Taskforce.sh
And coming up on the roadmap...
There are a few third-party UIs that you can use for monitoring:
BullMQ
Bull v3
Bull <= v2
Since there are a few job queue solutions, here is a table comparing them:
| Feature | BullMQ-Pro | BullMQ | Bull | Kue | Bee | Agenda |
|---|---|---|---|---|---|---|
| Backend | redis | redis | redis | redis | redis | mongo |
| Observables | ✓ | |||||
| Group Rate Limit | ✓ | |||||
| Group Support | ✓ | |||||
| Batches Support | ✓ | |||||
| Parent/Child Dependencies | ✓ | ✓ | ||||
| Priorities | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Concurrency | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Delayed jobs | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Global events | ✓ | ✓ | ✓ | ✓ | ||
| Rate Limiter | ✓ | ✓ | ✓ | |||
| Pause/Resume | ✓ | ✓ | ✓ | ✓ | ||
| Sandboxed worker | ✓ | ✓ | ✓ | |||
| Repeatable jobs | ✓ | ✓ | ✓ | ✓ | ||
| Atomic ops | ✓ | ✓ | ✓ | ✓ | ||
| Persistence | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| UI | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Optimized for | Jobs / Messages | Jobs / Messages | Jobs / Messages | Jobs | Messages | Jobs |
npm install bull --save
or
yarn add bull
Requirements: Bull requires a Redis version greater than or equal to 2.8.18.
npm install @types/bull --save-dev
yarn add --dev @types/bull
Definitions are currently maintained in the DefinitelyTyped repo.
We welcome all types of contributions, either code fixes, new features or doc improvements. Code formatting is enforced by prettier. For commits please follow conventional commits convention. All code must pass lint rules and test suites before it can be merged into develop.
const Queue = require('bull');
const videoQueue = new Queue('video transcoding', 'redis://127.0.0.1:6379');
const audioQueue = new Queue('audio transcoding', { redis: { port: 6379, host: '127.0.0.1', password: 'foobared' } }); // Specify Redis connection using object
const imageQueue = new Queue('image transcoding');
const pdfQueue = new Queue('pdf transcoding');
videoQueue.process(function (job, done) {
// job.data contains the custom data passed when the job was created
// job.id contains id of this job.
// transcode video asynchronously and report progress
job.progress(42);
// call done when finished
done();
// or give an error if error
done(new Error('error transcoding'));
// or pass it a result
done(null, { framerate: 29.5 /* etc... */ });
// If the job throws an unhandled exception it is also handled correctly
throw new Error('some unexpected error');
});
audioQueue.process(function (job, done) {
// transcode audio asynchronously and report progress
job.progress(42);
// call done when finished
done();
// or give an error if error
done(new Error('error transcoding'));
// or pass it a result
done(null, { samplerate: 48000 /* etc... */ });
// If the job throws an unhandled exception it is also handled correctly
throw new Error('some unexpected error');
});
imageQueue.process(function (job, done) {
// transcode image asynchronously and report progress
job.progress(42);
// call done when finished
done();
// or give an error if error
done(new Error('error transcoding'));
// or pass it a result
done(null, { width: 1280, height: 720 /* etc... */ });
// If the job throws an unhandled exception it is also handled correctly
throw new Error('some unexpected error');
});
pdfQueue.process(function (job) {
// Processors can also return promises instead of using the done callback
return pdfAsyncProcessor();
});
videoQueue.add({ video: 'http://example.com/video1.mov' });
audioQueue.add({ audio: 'http://example.com/audio1.mp3' });
imageQueue.add({ image: 'http://example.com/image1.tiff' });
Alternatively, you can return promises instead of using the done callback:
videoQueue.process(function (job) { // don't forget to remove the done callback!
// Simply return a promise
return fetchVideo(job.data.url).then(transcodeVideo);
// Handles promise rejection
return Promise.reject(new Error('error transcoding'));
// Passes the value the promise is resolved with to the "completed" event
return Promise.resolve({ framerate: 29.5 /* etc... */ });
// If the job throws an unhandled exception it is also handled correctly
throw new Error('some unexpected error');
// same as
return Promise.reject(new Error('some unexpected error'));
});
The process function can also be run in a separate process. This has several advantages:
In order to use this feature just create a separate file with the processor:
// processor.js
module.exports = function (job) {
// Do some heavy work
return Promise.resolve(result);
}
And define the processor like this:
// Single process:
queue.process('/path/to/my/processor.js');
// You can use concurrency as well:
queue.process(5, '/path/to/my/processor.js');
// and named processors:
queue.process('my processor', 5, '/path/to/my/processor.js');
A job can be added to a queue and processed repeatedly according to a cron specification:
paymentsQueue.process(function (job) {
// Check payments
});
// Repeat payment job once every day at 3:15 (am)
paymentsQueue.add(paymentsData, { repeat: { cron: '15 3 * * *' } });
As a tip, check your expressions here to verify they are correct: cron expression generator
A queue can be paused and resumed globally (pass true to pause processing for
just this worker):
queue.pause().then(function () {
// queue is paused now
});
queue.resume().then(function () {
// queue is resumed now
})
A queue emits some useful events, for example...
.on('completed', function (job, result) {
// Job completed with output result!
})
For more information on events, including the full list of events that are fired, check out the Events reference
Queues are cheap, so if you need many of them just create new ones with different names:
const userJohn = new Queue('john');
const userLisa = new Queue('lisa');
.
.
.
However every queue instance will require new redis connections, check how to reuse connections or you can also use named processors to achieve a similar result.
NOTE: From version 3.2.0 and above it is recommended to use threaded processors instead.
Queues are robust and can be run in parallel in several threads or processes without any risk of hazards or queue corruption. Check this simple example using cluster to parallelize jobs across processes:
const Queue = require('bull');
const cluster = require('cluster');
const numWorkers = 8;
const queue = new Queue('test concurrent queue');
if (cluster.isMaster) {
for (let i = 0; i < numWorkers; i++) {
cluster.fork();
}
cluster.on('online', function (worker) {
// Let's create a few jobs for the queue workers
for (let i = 0; i < 500; i++) {
queue.add({ foo: 'bar' });
};
});
cluster.on('exit', function (worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
} else {
queue.process(function (job, jobDone) {
console.log('Job done by worker', cluster.worker.id, job.id);
jobDone();
});
}
For the full documentation, check out the reference and common patterns:
If you see anything that could use more docs, please submit a pull request!
The queue aims for an "at least once" working strategy. This means that in some situations, a job could be processed more than once. This mostly happens when a worker fails to keep a lock for a given job during the total duration of the processing.
When a worker is processing a job it will keep the job "locked" so other workers can't process it.
It's important to understand how locking works to prevent your jobs from losing their lock - becoming stalled -
and being restarted as a result. Locking is implemented internally by creating a lock for lockDuration on interval
lockRenewTime (which is usually half lockDuration). If lockDuration elapses before the lock can be renewed,
the job will be considered stalled and is automatically restarted; it will be double processed. This can happen when:
lockDuration setting (with the tradeoff being that it will take longer to recognize a real stalled job).As such, you should always listen for the stalled event and log this to your error monitoring system, as this means your jobs are likely getting double-processed.
As a safeguard so problematic jobs won't get restarted indefinitely (e.g. if the job processor always crashes its Node process), jobs will be recovered from a stalled state a maximum of maxStalledCount times (default: 1).