BullMQは、Node.jsのバックグラウンドジョブをキューで捌くためのライブラリです。npmの最新は6.3.4(2026年9月1日公開)で、7月30日のv6.0.0からはRedisに加えてPostgreSQLもキューの保存先に選べるようになりました。この記事では、QueueとWorkerの役割分担、再試行・遅延・レート制限の指定方法、本番でつまずくRedis設定とジョブの二重実行、そしてBullMQを入れないほうが安く済む案件条件までを、公式ドキュメントとnpmレジストリの実測値で整理します。同期処理と非同期処理の方式そのものの整理は非同期処理の実装方式を比較した記事で扱っています。
まとめ:BullMQ採用の判断軸とv6で増えたバックエンド選択
BullMQを入れるかどうかは、再試行と遅延実行を自前のテーブルで管理し続けるコストと、Redis(またはPostgreSQL接続)と常駐ワーカーを1つ増やす運用コストの比較で決まります。指数バックオフ、キュー全体で共有されるレート制限、cron相当の定期実行、ジョブ間の依存関係までライブラリ側が持っているため、メール送信や画像変換のように失敗を前提とする処理を抱えたNode.jsアプリでは自作より収まりが良くなります。
2026年7月30日のv6.0.0で保存先が差し替え可能になり、Redisを立てずにPostgreSQLだけで同じQueue/Worker APIを回せます。構成要素は減る一方、並行処理の実測ではRedis側が約1.5〜2倍高速になるとドキュメントに明記された結果です。逆に、日次バッチ1本や月数千件程度しか流れないなら、cronとDBの1テーブルで足ります。そこへBullMQを入れるのは運用対象を増やすだけの選択になります。
Queue・Worker・QueueEventsで構成するBullMQのジョブ処理の流れ
BullMQのAPIは4つのクラスに分かれており、どれをどのプロセスに置くかで構成が決まります。ジョブを投入する側と処理する側を別プロセスへ分離できる点が、リクエスト処理の中で非同期処理を完結させる書き方との違いです。
Queue・Worker・QueueEventsとFlowProducerの役割分担
Queueはジョブの投入と管理を担当し、Workerが実際の処理関数を回します。QueueEventsはジョブの状態遷移をイベントとして受け取り、進捗表示や監視に使う購読専用クラスです。FlowProducerは親子関係を持つジョブ木を一括投入するためのもので、子ジョブがすべて完了してから親が動く依存関係を組めます。
- Queue:ジョブの追加・削除・スケジューラ登録
- Worker:処理関数の実行と並行度の制御
- QueueEvents:完了・失敗などの状態購読
- FlowProducer:親子ジョブ木の一括投入
最小構成はQueueとWorkerの2つです。WorkerはRedisへブロッキング接続を張り続けるため、HTTPサーバと同じプロセスに同居させるとリクエスト処理と実行時間を奪い合います。ワーカーは別プロセス、別コンテナで常駐させる前提に置いてください。
Bull(無印)からBullMQへ移行する際のAPI差分と型定義の扱い
BullMQは、Bull(無印)をRedis Streams基盤で書き直した後継です。Bullも動きますが機能追加は止まっており、TypeScriptの型定義を本体に同梱している点と、ワーカーのライフサイクルが整理されている点がBullMQ側の実利になります。移行で効くのは、Bullのprocess()がBullMQではWorkerのコンストラクタに処理関数を渡す形へ変わったことです。
v6.0.0ではさらに破壊的変更が入りました。Queue#client、Queue#redisVersion、Worker#blockingClient、FlowProducer#clientが削除され、Redisクライアントを直接触っていたコードは動きません。オプションのConnectionパラメータもBackendFactoryへ置き換わっています。レガシーなrepeatable jobs APIも削除済みで、定期ジョブはJob Schedulerへ寄せる必要があります。Worker#resume()が非同期化された点も、v5から上げる際に見落としやすい差分です。
再試行・遅延・レート制限をジョブオプションで作り込む実装指針
再試行、遅延実行、レート制限は、いずれもジョブ追加時のオプションかWorkerのオプションで指定します。自前実装なら作り込む部分がここに集約されるため、BullMQを入れる価値の大半はこの章の設定項目にあります。
attempts と exponential backoff で決める再試行間隔の設計
再試行回数はattempts、間隔はbackoffで決めます。typeにexponentialを指定した場合の待ち時間は 2^(試行回数-1) × delay ミリ秒で、delayを1000にすると1秒・2秒・4秒と伸びる計算です。外部APIが落ちているときに全ワーカーが同じ間隔で叩き直す事態を避けたいなら、0〜1で指定するjitterを併用します。jitter 0.5・delay 1000なら500〜1000ミリ秒の範囲へ散らばります。
固定間隔なら type にfixedを指定します。独自の計算式を使いたいときは、Worker側のbackoffStrategyへ関数を渡してください。戻り値0で待機キューの末尾へ移動し、-1で即座に失敗状態へ遷移します。入力そのものが不正でリトライしても回復しないケースは、UnrecoverableErrorを投げれば残りの試行回数を消費せずに失敗させられます。なお、スローする例外にはErrorオブジェクトが必要です。
limiterのmaxとdurationがワーカー横断で効くレート制限
Workerのオプションにlimiterを置き、maxとdurationを指定すると、その期間内に処理するジョブ数の上限が決まります。max 10・duration 1000なら毎秒10件です。取り違えやすいのが適用範囲で、この上限はワーカープロセスごとではなくキュー全体で共有されます。ワーカーを10台へ増やしても合計で毎秒10件を超えません。外部APIの契約レートに合わせて上限を引く用途へそのまま使えます。
相手が返す429を見て動的に絞りたい場合は、worker.rateLimit(duration)を呼んでからWorker.RateLimitError()を投げます。こうするとジョブは失敗ではなく待機状態へ戻り、試行回数を消費しません。現在の制限状態はgetRateLimitTtl()で確認でき、removeRateLimitKey()で解除できます。PHP側で同じ構造を組む場合の比較はLaravel Queueのジョブ定義とworker常駐の記事にまとめてあります。
遅延ジョブとJob Schedulerのeveryとpattern指定の使い分け
指定時刻まで待たせるだけならdelayにミリ秒を渡します。繰り返し実行はv6でJob Schedulerへ一本化され、upsertJobSchedulerにスケジューラIDと設定を渡して登録する形になりました。everyはミリ秒の固定間隔、patternはcron式です。ドキュメントの例にある「0 15 3 * * *」は毎日3時15分を指します。
upsertという名前のとおり、同じIDで呼び直せば設定が上書きされます。デプロイのたびに定期ジョブが二重登録されるという、旧repeatable jobs時代の事故はここで解消されました。注意したいのはcron式の書式で、ドキュメントの例は秒フィールドを含む6フィールド表記です。5フィールド前提で書いたUnix cronの設定をそのまま貼るとフィールドが1つずれます。移行時は先頭フィールドの意味を必ず確認してください。
v6で追加されたPostgreSQLバックエンドとRedis構成の選択基準
v6.0.0の目玉は、キューの保存先を差し替えられるようにしたことです。IQueueBackendという抽象が入り、RedisバックエンドとPostgreSQLバックエンドの2実装が同梱されました。
createPostgresBackendの指定手順とPostgreSQL 13以上
PostgreSQLで動かすにはpgパッケージを入れ、Queueの第3引数にcreatePostgresBackendを渡します。接続の指定は、接続文字列、プール設定オブジェクト、既存のpg.Poolインスタンスの3形式に対応します。pg.Poolを渡した場合、そのライフサイクル管理はBullMQ側ではなく呼び出し側の責任です。アプリ全体の既定にするならsetDefaultBackendFactoryを起動時に一度呼びます。
サーバ要件はPostgreSQL 13以上で、ドキュメントは14以上を推奨しています。バージョンは初回接続時に検査され、条件を満たさない場合はUnsupportedPostgresVersionErrorが投げられます。npmのpeerDependenciesはいずれもoptionalで、pgは8.0.0以上、ioredisとredisは5.0.0以上という指定です。v6からioredisが直接依存ではなくなったため、Redisで使う構成でも明示的なインストールが要ります。
並行処理でRedisが約1.5〜2倍速いという性能差の許容条件
機能面の差は小さく、キュー、ワーカー、フロー、ジョブスケジューラ、レート制限、優先度、遅延ジョブ、重複排除、メトリクス、イベントはPostgreSQLバックエンドでも動きます。落ちるのはRedis固有の情報で、getBackend()はRedisのバージョン情報などを返しません。差が出るのは性能面です。
| 観点 | Redis | PostgreSQL |
|---|---|---|
| 順序処理の速度 | 同等 | 同等 |
| 並行処理の速度 | 約1.5〜2倍速い | 基準 |
| 増える運用対象 | Redisを1台 | 既存DBで完結 |
| 最低バージョン | 6.2.0以上 | 13以上(14推奨) |
判断はこう置きます。ジョブが秒間数百件を超える、あるいはワーカーの並行度を上げて捌く設計ならRedisです。日次のバッチや管理画面からの非同期処理が中心で、件数よりも構成要素を減らしたい案件ならPostgreSQLで足ります。すでにRedisをセッションやキャッシュに使っているなら、そこへ相乗りさせるほうが早く済みます。
本番運用で詰まるRedis設定・ワーカー停止・ジョブ二重実行の対処
BullMQで事故が起きるのは、アプリのコードよりもRedis側の設定とプロセスの止め方です。ここを外すとジョブが静かに消えます。
maxmemory-policyのnoevictionとAOF有効化が要る理由
Redisのmaxmemory-policyはnoevictionに設定する必要があります。LRUなどの追い出しポリシーのままだと、メモリが埋まった時点でBullMQが使うキーが黙って削除され、ジョブの喪失や原因不明のエラーとして表面化します。ドキュメントによれば、この設定はキューの正常動作を保証する唯一の方法です。永続化はAOFを有効にし、フラッシュ間隔は1秒で足りるとされています。
自前でioredisクライアントを作ってWorkerへ渡す構成では、maxRetriesPerRequestをnullにしてください。この設定を欠くとBullMQが例外を投げます。マネージドのRedisを使うなら、追い出しポリシーを変更できるプランかどうかを先に確認してください。サーバーレスRedisであるUpstashの料金と制約のように、接続数やコマンド課金の条件はホスティングごとに違うため、ワーカー台数を決める前に確認しておきます。
stalledジョブと少なくとも1回実行を前提にした冪等な設計
BullMQの配送保証は少なくとも1回です。ワーカーが処理の途中で落ちると、そのジョブはstalled(停滞)と判定され、別のワーカーへ再投入されます。プロセスがOOMで殺された場合も経路は同じです。つまり同じジョブが2回走る前提でハンドラを書きます。決済APIの呼び出しやメール送信のように副作用のある処理では、ジョブIDや業務キーで実行済みかどうかを判定してから本処理へ入る設計にしてください。
停止時はWorkerのclose()を呼び、処理中のジョブを完了させてから抜けます。SIGINTとSIGTERMを受けてclose()を呼ぶだけで、デプロイのたびにジョブが停滞する状態を回避できます。あわせてremoveOnCompleteとremoveOnFailを指定してください。既定では完了・失敗したジョブが残り続けるため、放置するとRedisのメモリを食い潰します。ジョブのdataフィールドが平文で保存される点にも注意が必要です。個人情報や資格情報はジョブの外へ置くか、暗号化してから渡します。
BullMQを見送るべき案件条件とマネージドキューとの分岐点
ここからは採用可否の線引きです。BullMQは入れれば便利ですが、常駐ワーカーとデータストアという運用対象が増えます。増やす価値が出る境目を示します。
月数千件・単一ワーカーならcronとDBテーブルで足りる損益分岐
月数千件、ワーカー1台、遅延実行もレート制限も要らない。この条件ならBullMQは過剰です。cronで叩くスクリプトと、状態列を持つDBテーブル1枚で足ります。BullMQの価値は、複数ワーカーへの分散、指数バックオフ付きの再試行、キュー全体で共有されるレート制限、ジョブ間の依存関係という4点にあります。このどれも使わないなら、増えるのは運用対象だけです。
マネージド側へ寄せる選択肢もあります。クラウド上でHTTPエンドポイントを叩くだけの非同期処理なら、Cloud Tasksのリトライ仕様と料金体系のようなマネージドキューのほうが、ワーカー常駐を持たずに済みます。BullMQが有利なのは、処理をNode.jsのコードとして同じリポジトリへ置き、Flowsで依存関係を組み、レート制限をライブラリ側へ任せたい場合です。クラウドのキュー製品へ寄せると、この3点は自前で書き直す羽目になります。
BullMQ Proのグループ単位レート制限が要るマルチテナント条件
BullMQ Proは有償版で、Groups、グループ単位のレート制限、グループ単位の並行度制御、Observables、Batches、Telemetryが追加されます。標準版のドロップイン置き換えとして設計されており、ライセンスは組織単位で、契約後はプロジェクト数の制限なく使える仕組みです。無料のトライアルトークンを請求して評価してから判断できます。
買う価値が出るのはマルチテナントのSaaSです。テナントAが1万件を投入した直後にテナントBの1件が延々と後ろで待たされる、という問題は無償版のFIFOでは解けません。グループ単位の並行度と制限があれば、テナントごとに枠を切って公平に回せます。テナント概念のない単一システムなら無償版で十分です。こうしたキューを含む非同期処理基盤を、設計から実装まで外部へ任せたい場合はAPI開発・システム連携で相談を受け付けています。
よくある質問
BullMQの導入検討でよく挙がる質問を、公式ドキュメントとnpmレジストリの実測値をもとに整理します。
BullMQとBull(無印)はどちらを選べばよいですか?
新規ならBullMQです。BullMQはBullをRedis Streams基盤で書き直した後継で、TypeScriptの型定義を同梱し、Job SchedulerやFlowsといった機能もBullMQ側にしかありません。Bullは動作こそしますが機能追加は止まっています。既存のBull資産がある場合も、処理関数の渡し方がprocess()からWorkerのコンストラクタへ変わる形が中心なので、移行の負担はそれほど大きくなりません。
Redisはどのバージョンから使えますか?
ドキュメントは6.2.0以上でフルに準拠すると記載しています。それより古いRedisでも一部は動きますが、機能差でつまずくため6.2.0以上を用意してください。あわせてmaxmemory-policyをnoevictionにする設定が要ります。マネージドRedisやRedis互換の実装を使う場合は、すべてがBullMQで正しく動くわけではないとドキュメント自身が注意しているため、採用前に検証環境で通しの動作確認を行います。
ジョブが処理されないときは何から確認しますか?
まずWorkerがそのキュー名で起動しているかを見ます。QueueとWorkerでキュー名の文字列が食い違っているケースが最も多い原因です。次にRedisの接続先が投入側と処理側で一致しているかを確認します。それでも動かない場合は、レート制限がかかっていないかをgetRateLimitTtl()で調べ、遅延ジョブとして未来の時刻に積まれていないかを見ます。v6へ上げた直後なら、削除されたQueue#clientなどに依存していないかも確認対象です。
完了したジョブはいつ消えますか?
既定では消えません。完了したジョブも失敗したジョブも、Redis(またはPostgreSQL)に残り続けます。removeOnCompleteとremoveOnFailを指定して、件数か経過時間で保持範囲を絞ってください。失敗ジョブは調査のためにある程度残す運用が現実的なので、完了ジョブは短く、失敗ジョブは長く保持する非対称な設定にする例が多く見られます。
PostgreSQLバックエンドは本番で使えますか?
v6.0.0で正式に同梱された機能で、キュー、ワーカー、フロー、ジョブスケジューラ、レート制限、優先度、遅延ジョブ、重複排除、メトリクス、イベントが動きます。性能面には制約があり、並行処理ではRedis側が約1.5〜2倍高速との記載です。件数が少なく、Redisを追加したくない構成であれば選択肢になります。スループットを取りに行く案件ではRedisを選んでください。
関連記事
- Sidekiqとは?Rubyで非同期処理を実現するバックグラウンドジョブツール:Ruby側で同じ役割を担うライブラリ。Redisを前提とする設計の共通点と差を確認できます。
- Valkeyとは?Redisとの違い・導入方法・データ型を2026年最新版で解説:Redis互換実装を選ぶ際の判断材料。BullMQが要求する6.2.0相当の機能を満たすかの検討に使えます。
- スタックとキューの違い|FIFO・LIFOと使い分け・Python実装を図解:データ構造としてのキューの整理。BullMQが前提とするFIFOの挙動を押さえたい場合に。
- Celery(Python)とは:分散タスクキューの仕組みとブローカー選定・冪等性設計を解説:Python側で同じ役割を担うライブラリ。ブローカーの選び分けと再試行設計をBullMQと比較できます。