TL;DR クイックフィックス
Worker Thread が V8 ヒープ制限に達しました — 64ビットシステムではデフォルトで512MBです。最速の修正方法は、ワーカーを生成する際にヒープサイズを増やすことです:
const { Worker } = require('worker_threads');
const worker = new Worker('./my-worker.js', {
resourceLimits: {
maxOldGenerationSizeMb: 1024, // 1 GB
maxYoungGenerationSizeMb: 128
}
});
それでもクラッシュする、またはメモリが修正後も増加し続ける場合は、読み進めてください。おそらくワーカー内にメモリリークがあり、制限を引き上げても次のクラッシュまでの時間を稼いでいるだけです。
このエラーが発生する原因
完全なエラーは次のように表示されます:
Uncaught Error: ERR_WORKER_OUT_OF_MEMORY: Worker terminated due to reaching memory limit: JS heap could not be allocated
Worker Thread は独自の分離された V8 コンテキストと専用ヒープ上で動作します。ワーカーが制限を超えてメモリを確保しようとすると、V8 は即座にそれを終了させ、このエラーを親スレッドに返します。
主な原因は以下の通りです:
- 非常に大きな JSON ファイルやデータセットを一度に処理している
- ストリーミングやバッチ処理なしに増大し続ける配列に結果を蓄積している
- メモリリーク — クロージャ、イベントリスナー、または解放されないキャッシュオブジェクト
SharedArrayBufferやBufferを解放せずに使用している- 参照を保持し続ける大きなコールスタックを積み上げる再帰処理
修正1:ワーカーのメモリ制限を増やす
Worker を作成する際に resourceLimits を渡して、ヒープの上限を引き上げます:
// parent.js
const { Worker } = require('worker_threads');
const worker = new Worker('./heavy-task.js', {
resourceLimits: {
maxOldGenerationSizeMb: 2048, // 2 GB for old gen heap
maxYoungGenerationSizeMb: 256 // 256 MB for young gen
}
});
worker.on('error', (err) => {
console.error('Worker error:', err.message);
});
worker.on('exit', (code) => {
if (code !== 0) console.error(`Worker stopped with exit code ${code}`);
});
適している場合: 大きなヒープが本当に必要なタスク — たとえば、500 MB の JSON ファイル全体をメモリ上でパースするケース。メモリ使用量は高くても、既知の上限がある場合です。
適していない場合: メモリが上限なく増加し続ける場合。それはリークです。制限を引き上げても、クラッシュを数分遅らせるだけです。
修正2:大量データをすべて一度にロードせず、ストリームまたはバッチで処理する
ファイル全体をメモリにロードすることが最もよくある原因です。代わりに行ごとのストリーミングに切り替えましょう:
// heavy-task.js (worker)
const { parentPort } = require('worker_threads');
const fs = require('fs');
const readline = require('readline');
async function processLargeFile(filePath) {
const rl = readline.createInterface({
input: fs.createReadStream(filePath),
crlfDelay: Infinity
});
let count = 0;
for await (const line of rl) {
// 一度に1行ずつ処理 — ファイル全体の配列をメモリに保持しない
count++;
}
parentPort.postMessage({ count });
}
processLargeFile('/data/large-log.txt');
大きな配列の場合は、固定サイズのバッチで処理し、中間結果を親に送信します。すべてを先に収集しないようにしましょう:
// 悪い例:
const results = items.map(expensiveTransform); // すべてをメモリに保持
parentPort.postMessage(results);
// 良い例:
const BATCH_SIZE = 1000;
for (let i = 0; i = items.length });
// 次のバッチ前にGCに処理を譲る
await new Promise(resolve => setImmediate(resolve));
}
修正3:ワーカー内のメモリリークを見つけて修正する
上限なくメモリが増加する場合はリークです。ワーカーの先頭に定期的なチェックを追加して早期に検出しましょう:
// ワーカーファイルの先頭に追加
const CHECK_INTERVAL_MS = 5000;
setInterval(() => {
const { heapUsed, heapTotal } = process.memoryUsage();
console.log(`[Worker] Heap: ${Math.round(heapUsed / 1024 / 1024)} MB / ${Math.round(heapTotal / 1024 / 1024)} MB`);
}, CHECK_INTERVAL_MS).unref(); // .unref() でワーカーの終了をブロックしない
ほとんどのワーカーリークは以下の4つのパターンが原因です:
- イベントリスナーが削除されていない: 完了時に
emitter.removeAllListeners()を呼び出すか、一度きりのハンドラにはon()の代わりにonce()を使用する。 - 無制限のキャッシュ: キャッシュとして使用される
Mapやオブジェクトは、エントリを削除しない限り永遠に増え続けます。固定サイズ制限付きの LRU キャッシュライブラリの使用を検討してください。 - 参照を保持するクロージャ: ループ内のコールバックはクロージャを通じて大きな配列を暗黙的に保持することがあります。注意深く確認しましょう。
- クリアされていないタイマー:
setIntervalはコールバックとそのクロージャ内のすべてへの参照を保持します。処理が完了したらインターバルをクリアしてください。
// リーク: 'log' は100msごとに1エントリ増加し続け、縮小しない
let log = [];
const interval = setInterval(() => {
log.push(Date.now());
}, 100);
// 修正: 処理完了後にインターバルをクリアして参照を解放する
clearInterval(interval);
log = null;
修正4:プロセス全体に --max-old-space-size を使用する
Node.js の起動方法を制御でき、すべてのワーカーにより多くのメモリが必要な場合は、プロセスレベルで V8 フラグを設定します:
node --max-old-space-size=4096 parent.js
これにより、独自の resourceLimits を指定していないワーカーを含め、Node.js プロセス全体に 4 GB の旧世代ヒープが設定されます。すべてを一度にカバーする手軽な上限引き上げ方法です。本番環境では、ワーカーごとの resourceLimits を優先してください — それにより精密な制御が可能になり、暴走したワーカーが他のワーカー用のメモリを消費するのを防げます。
修正が効いたか確認する
ワーカーが正常に終了したことを確認し、最終的なヒープ使用量を報告するメッセージハンドラを設定します:
// parent.js
worker.on('message', (msg) => {
if (msg.done) {
console.log('Worker completed successfully. Heap at exit:', msg.heapUsed);
}
});
worker.on('error', (err) => {
// ERR_WORKER_OUT_OF_MEMORY はここに表示されなくなるはずです
console.error('Worker failed:', err.code, err.message);
});
// worker.js — 完了時にヒープ使用量を報告する
const { heapUsed } = process.memoryUsage();
parentPort.postMessage({ done: true, heapUsed });
タスク実行中の常駐メモリを監視します:
# nodeプロセスのRSSをリアルタイムで監視する
node --expose-gc parent.js &
watch -n 1 "ps -o pid,rss,vsz,comm -p $(pgrep -n node)"
終了コード 0 でのクリーンな終了と、エラーハンドラに ERR_WORKER_OUT_OF_MEMORY が表示されなければ成功です。RSS が新しい上限まで増加し続ける場合は、修正2と修正3に戻ってください — 制限の引き上げは効果がありましたが、根本的な問題はまだ残っています。
クイックリファレンス
- 既知サイズの一回限りの大規模タスク:
resourceLimitsのmaxOldGenerationSizeMbを引き上げる - 大きなファイルの処理: ストリームと
readlineを使用し、ファイル全体を配列にロードしない - メモリが無制限に増加する: リークがあります — リスナー、キャッシュ、タイマーを監査する
- すべてのワーカーにより多くのメモリが必要: 起動時に
--max-old-space-sizeを使用する

