バックプレッシャー(Backpressure)は、受信側(consumer)が処理できる量に合わせて、送信側(producer)へ速度を落とす必要を伝える仕組みです。
速いproducerと遅いconsumerの間に無制限キューを置くと、遅延とメモリ使用量が増え続けます。 待つ、上限を設ける、古いデータを捨てる、要求を拒否する、という選択を要件に合わせて行います。
過負荷が連鎖する流れ
producerが毎秒100件を送り、consumerが毎秒60件しか処理できない場合、差分の40件が毎秒キューへ残ります。短いburstなら吸収できますが、継続すればキュー待ち時間とメモリが増えます。
producer 100件/秒 -> queue +40件/秒 -> consumer 60件/秒
キューの上限がなければ、いずれメモリ不足、タイムアウト、再試行の増加が起きます。再試行が新しい負荷を作り、依存サービスまで遅くなることもあります。

基本戦略
| 戦略 | 向く状況 | 失うもの・注意点 |
|---|---|---|
| producerを待たせる | 一件も失えず、送信側が待てる | 待ち時間と上流の占有が増える |
| 上限付きキュー | 短いburstを吸収したい | 上限到達時の動作が別に必要 |
| batch化 | まとめて処理すると効率が上がる | 一件目の待ち時間が増える |
| 古い/新しい項目をdrop | 最新値だけ重要な監視データなど | 欠落を許せる契約が必要 |
| rejectする | これ以上受けると全体が壊れる | 429/503と再試行方針が必要 |
すべてを保存する要件なら、dropではなく永続キュー、送信側の停止、処理能力の増強を組み合わせます。メモリ上のキューを大きくするだけでは耐久性は得られません。
Node.js Streamsの最小例
Node.jsのWritable.write()は、内部バッファがしきい値へ達するとfalseを返します。その後も書き続けることはできますが、メモリへ蓄積し続けるため、drainを待ちます。
import { once } from "node:events";
async function writeAll(writable, chunks) {
for await (const chunk of chunks) {
if (!writable.write(chunk)) {
await once(writable, "drain");
}
}
writable.end();
await once(writable, "finish");
}
ファイルや変換処理をつなぐ場合は、可能ならstream.pipeline()を使うと、バックプレッシャーとエラー伝播をまとめて扱えます。highWaterMarkは「最大メモリ量」そのものではなく、読み書きを止める判断に使うしきい値です。
Reactive Streamsの要求量
Reactive Streamsでは、subscriberがrequest(n)で受け取れる個数を示し、publisherは要求された数を超えて送らない契約を中心にしています。ライブラリを使う時は、演算子がbuffer、drop、最新値のみ保持のどれを行うか確認します。
バックプレッシャー対応をうたうAPIでも、途中で通常の配列へ全件展開したり、無制限キューへ渡したりすれば制御は切れます。producerから最終consumerまで、境界ごとの上限を追います。
HTTP APIでの過負荷制御
同期HTTPでは、下流が飽和した時に要求を無制限に保持せず、早めに429 Too Many Requestsまたは503 Service Unavailableを返す設計があります。Retry-Afterを付けられる場合でも、全クライアントが同時に再試行しないよう指数バックオフとjitterを使います。
if queue.isFull():
return 503 with Retry-After
queue.enqueue(job)
return 202 with operationId
202 Acceptedを返すなら、処理の永続性、状態確認、失敗通知、重複要求の扱いをAPI契約へ含めます。受付後にメモリから消える構成では「受理済み」を保証できません。
ジョブキューでの設計
非同期ジョブでは、producerが受付API、consumerがworkerになります。キューに空きがあるだけで受け付け続けると、待ち時間がサービスの期限を超える場合があります。件数上限に加えて「今受けた仕事が期限内に終わるか」を判断します。
優先度がある場合、低優先度ジョブがキューを埋めて重要ジョブを妨げないよう、キューや同時実行数を分けます。失敗ジョブは無限に先頭へ戻さず、再試行回数、backoff、dead-letter queue、手動再処理を設計します。
consumer数を増やす時も、DB接続数、外部APIのquota、ロック競合を上限として扱います。workerだけ増やして下流を飽和させると、全体の処理率がかえって下がることがあります。
WebSocketや画面更新
リアルタイム画面でproducerのイベントをすべて描画すると、ブラウザの描画が追いつかない場合があります。株価や監視値のように最新状態が重要なら、中間値を間引き、一定間隔で最新値だけ描画できます。
一方、チャットや監査イベントのように欠落できないデータはdropせず、永続化した履歴と再開位置を用意します。同じ「ストリーム」でも、イベントの意味によって戦略が変わります。
キャンセルとエラーを上流へ返す
consumerが処理を中止したのにproducerが作業を続ければ、不要なCPU、I/O、メモリを使います。接続切断、タイムアウト、処理失敗をキャンセル信号として上流へ伝えます。
Node.jsでは複数のstreamを手作業でpipe()するより、Promise版のpipeline()を使うと、完了とエラーを一つの処理として待てます。
import { pipeline } from "node:stream/promises";
import { createReadStream, createWriteStream } from "node:fs";
import { createGzip } from "node:zlib";
await pipeline(
createReadStream("input.log"),
createGzip(),
createWriteStream("input.log.gz"),
);
途中のtransformが全データを配列へためる実装なら、streamを使っていても上限は失われます。各段が一件ずつ、または決めたbatch単位で処理することを確認します。
順序、公平性、重複
並行consumerを増やすと、完了順が入力順と変わることがあります。順序が必要なら、partition keyごとに直列化する、sequence番号で並べ直す、並行数を制限する、といった設計が必要です。
一つの大きなジョブがキューを占有すると、小さなジョブが待ち続けるhead-of-line blockingも起きます。サイズ別キューや公平なschedulerを検討します。
タイムアウト後に再試行されると、最初の処理も完了して重複する可能性があります。consumerはidempotency keyを使い、同じジョブを二度適用しても副作用が重複しないようにします。
似た仕組みとの違い
- Rate limiting:利用者や期間ごとの到着量を制限する。consumerの現在容量を直接返すとは限りません。
- Load shedding:過負荷時に一部の仕事を捨て、重要な処理を守ります。
- Circuit breaker:失敗中の依存先への呼び出しを一時的に止め、連鎖障害を防ぎます。
- Autoscaling:処理能力を増やしますが、起動までの遅れがあるため、上限付きキューの代わりにはなりません。
これらは競合する仕組みではなく、入口の制限、内部のフロー制御、依存先の保護として組み合わせます。
上限を決める
キュー件数だけでなく、キュー内の総バイト数と待ち時間を見ます。画像一件と小さなイベント一件ではメモリ量が違うからです。
確認する指標は次です。
- queue depthと最古メッセージの待ち時間
- producerの入力率とconsumerの処理率
- メモリ、CPU、外部I/O
- drop/reject件数
- 処理時間のpercentileとタイムアウト
- 再試行数とdead-letter queue
上限到達後の動作をテストしないと、普段の負荷では見えないデータ欠落や停止が本番で起きます。短いburst、継続的な過負荷、consumer停止、復旧後の滞留解消を別々に試します。
上限から逆算する
たとえば処理期限が30秒、consumerが安定して毎秒50件を処理できるなら、単純計算で1500件を超える待ち行列は期限を守れません。実際には処理時間のばらつきと失敗分を見込み、さらに小さい上限にします。
この計算は固定の性能値ではありません。負荷試験で処理率とpercentileを測り、デプロイや依存先の変化に合わせて更新します。
よくある誤解
- キューを大きくすれば解決する:故障までの時間を延ばすだけで、継続的な速度差は残ります。
- dropは必ず悪い:最新値だけ必要なデータでは、古い値を捨てる方が正しい場合があります。
- 再試行すれば安全:同時再試行は負荷を増やすため、上限、backoff、jitterが必要です。
- consumerを増やせばよい:DBや外部APIが新しいボトルネックになる場合があります。
まとめ
バックプレッシャーは、consumerの処理能力をproducer側の動作へ反映する仕組みです。上限付きキューを基準に、待機、batch、drop、rejectをデータの重要度に応じて選びます。キュー件数だけでなく待ち時間とバイト数を観測し、過負荷と復旧の両方を試します。