
n8nのCodeノードだけが「JavaScript heap out of memory」で止まった経験はありませんか。他のノードは正常なのにCodeノードだけ落ちると、原因が掴みにくく困りますよね。実はこれ、V8エンジンのメモリ管理とCodeノードの特性が深く関係しています。
そこで今回は、セルフホスト環境のn8nで「JavaScript heap out of memory」エラーの原因究明と解決方法の解説を行ってみました!初心者の方でも実践できる内容ですので、ぜひ最後まで読んで対処に活かしてください!
この記事で分かること
- JavaScript heap out of memoryが発生する仕組み
- 原因を「データ量」と「インスタンス設定」で切り分ける方法
- NODE_OPTIONSの設定手順とSplit In Batchesを使った具体的な解決策
JavaScript heap out of memoryが起きる仕組み
n8nはNode.js上で動作しており、内部でV8エンジンがJavaScriptを実行しています。V8はメモリ領域を「New Space」と「Old Space」に分けて管理しています。
短命なオブジェクトはNew Spaceで一時的に処理され、長く生き残るデータはOld Spaceに移動します。このOld Spaceには上限が設けられており、超過するとエラーで処理が強制終了します。
Codeノードが特にメモリを消費しやすいのは、次のような処理特性があるためです。
- 受け取ったすべてのアイテムを一度にJavaScriptオブジェクトとしてメモリ展開する
- ループ処理やJSON変換のたびに一時的なデータコピーが大量に生成される
- 処理が完了するまでガベージコレクション(不要メモリの解放)が働きにくい
CodeノードやFunctionノードは、標準ノードと比べてメモリ消費が大きい部類に入ります。データ量が多いワークフローほど、この特性の影響を受けやすくなります。
原因を切り分ける2つのケース
1回の実行で扱うデータ量が多すぎるケース
最も多い原因は、1回の実行でCodeノードに渡されるデータ量が大きすぎることです。
数千件のJSONデータや、大きなバイナリファイルをまとめて処理しようとすると、瞬時にOld Spaceを圧迫します。
特にAPIから一括取得したデータをそのままCodeノードで加工する構成は要注意です。
例えばPDFファイルをBase64文字列に変換する処理は、元データより約1.3倍サイズが膨らむため、大きなファイルほどメモリを圧迫しやすくなります。
実際にデータ量が原因かどうかは、Codeノードの直前に以下のコードを一時的に挿入して確認できます。
// ファイル名: check_data_size.js
// 直前のノードから受け取ったデータのサイズを確認する
const jsonString = JSON.stringify(items);
const sizeInMB = (Buffer.byteLength(jsonString, 'utf8') / 1024 / 1024).toFixed(2);
console.log(`アイテム数: ${items.length}件`);
console.log(`データサイズ: ${sizeInMB}MB`);
return items;
このコードの実行結果をn8nの実行ログで確認し、数十MBを超えていればデータ量過多が原因と判断できます。
このケースでは、後述するSplit In Batchesによる分割が有効です。
セルフホストのインスタンス自体のメモリ割り当てが少ないケース
もう一つの原因は、n8nを動かすサーバー自体のメモリ割り当てが不足していることです。
DockerコンテナやVPSでメモリ上限を低く設定していると、データ量が普通程度でもエラーが発生します。
サーバー側の実際のメモリ使用状況は、以下のコマンドで確認できます。
# ファイル名: check_memory.sh
# Dockerコンテナのメモリ使用量をリアルタイム確認
docker stats n8n --no-stream
# ホストサーバー全体の空きメモリを確認
free -h
docker statsの出力で「MEM USAGE / LIMIT」がほぼ上限に張り付いている場合は、コンテナ側のメモリ制限が原因です。
docker-compose.ymlでmem_limitを設定している場合は、その値がNODE_OPTIONSの設定値を下回っていないか必ず確認してください。
解決する3つの方法
NODE_OPTIONSで--max-old-space-sizeを増やす設定手順
まず試すべきは、V8のOld Space上限を引き上げる方法です。
Node.jsのNODE_OPTIONS環境変数に--max-old-space-sizeを指定することで、デフォルトのヒープ上限を明示的に拡張できます。
手順1:サーバーの搭載メモリを確認します。
# ファイル名: check_total_memory.sh
free -m | grep Mem | awk '{print $2}'
手順2:搭載メモリの70%程度を目安に設定値を計算します(例:8GB=8192MBなら約5700MB)。
手順3:Docker Composeで運用している場合の設定例です。
# ファイル名: docker-compose.yml
services:
n8n:
image: docker.n8n.io/n8nio/n8n
ports:
- "5678:5678"
mem_limit: 8g
environment:
- NODE_OPTIONS=--max-old-space-size=5700
手順4:コマンドラインから直接起動する場合はこちらです。
# ファイル名: start_n8n.sh
export NODE_OPTIONS="--max-old-space-size=5700"
n8n start
手順5:設定が反映されているかは、以下のコードをCodeノードで実行して確認します。
// ファイル名: check_heap_limit.js
// V8のヒープ上限をn8n上から確認する
const v8 = require('v8');
const heapStats = v8.getHeapStatistics();
const limitMB = (heapStats.heap_size_limit / 1024 / 1024).toFixed(0);
return [{ json: { heap_size_limit_mb: limitMB } }];
この結果が設定値に近い数値であれば、正しく反映されています。反映されていない場合はコンテナの再作成(docker compose up -d --force-recreate)を試してください。
Codeノードでの処理を標準ノードに置き換える判断基準
すべての処理をCodeノードで書くのではなく、標準ノードで代替できないか検討することも重要な対策です。
例えば、以下のようなCodeノードの集計処理があるとします。
// ファイル名: before_code_node.js
// 売上データを商品IDごとに合計する(メモリを消費しやすい書き方)
const grouped = {};
for (const item of items) {
const id = item.json.productId;
if (!grouped[id]) grouped[id] = 0;
grouped[id] += item.json.amount;
}
return Object.entries(grouped).map(([id, total]) => ({
json: { productId: id, total }
}));
この処理は、Codeノードを使わずにAggregateノードの「Field to Group By」にproductId、「Field to Aggregate」にamount(Sum)を指定するだけで代替できます。
標準ノードはn8n内部でバッチ処理される設計のため、同じ結果でもメモリの使用ピークを大きく下げられます。
置き換えの判断基準は次の通りです。
- 合計・カウント・グループ化 → Aggregateノードに置き換える
- 条件による絞り込み → Filterノード、IFノードに置き換える
- 複数データソースの結合 → Mergeノードに置き換える
- 正規表現や外部ライブラリが必要な処理のみ → Codeノードに残す
Split In Batchesでデータを分割してからCodeノードに渡す設計例
大量データを扱う場合は、Loop Over Items(Split In Batches)ノードで事前にデータを分割します。
具体的な設計手順は以下の通りです。
- データ取得ノードの直後にLoop Over Itemsノードを配置する
- 「Batch Size」を100〜500件程度に設定する(メモリ状況に応じて調整)
- ループの内側にCodeノードを配置し、バッチ単位のデータのみを処理させる
- ループの出口を再度Loop Over Itemsの入力に接続し、全バッチが終わるまで繰り返す
Codeノード内の処理例は以下の通りです。
// ファイル名: process_batch.js
// バッチごとに渡されたitemsのみを処理する
const results = [];
for (const item of items) {
const processed = {
id: item.json.id,
value: item.json.value * 2,
};
results.push({ json: processed });
}
return results;
さらにメモリ効率を高めたい場合は、この処理をサブワークフロー化します。親ワークフロー側では以下のようにExecute Workflowノードを設定します。
// ファイル名: parent_workflow_note.js
// Execute Workflowノードのパラメータ設定例(実際はGUI上で設定)
// Source: Database(サブワークフローIDを指定)
// Mode: Run once for each item
// Options > Wait For Sub Workflow Completion: true
サブワークフローは処理後の結果のみを親ワークフローに返す仕組みのため、大量データの中間状態が親側のメモリに残り続けるのを防げます。
Codeノードを安全に使うための設計原則
Codeノードを長期的に安定運用するための原則をまとめました。
- 大量データは必ず分割する
1回の実行で扱うアイテム数を100〜500件程度に抑え、Split In Batchesを併用します。 - バイナリデータはファイルシステムモードで扱う
環境変数N8N_DEFAULT_BINARY_DATA_MODE=filesystemを設定し、ファイルをRAMではなくディスクに保持します。 - 手動実行は最小限のテストデータで行う
本番相当の大量データで手動実行すると、UI表示用のコピーが余計に発生するため避けます。 - 不要な変数はループ内で使い回さない
大きな配列やオブジェクトをループの外に保持し続けないコードを心がけます。 - 実行時間に上限を設ける
長時間実行するワークフローには、タイムアウトなどのガード処理を組み込みます。
よくある質問
Q1. メモリを増やしても解決しない場合は?
コンテナ自体のメモリ上限が低い、もしくは過去の実行履歴がデータベースに蓄積し続けている可能性があります。
以下のように環境変数を設定すると、古い実行データを自動削除し、内部データベースの肥大化を防げます。
# ファイル名: docker-compose.yml(環境変数追加分)
environment:
- EXECUTIONS_DATA_MAX_AGE=168
- EXECUTIONS_DATA_PRUNE=true
Q2. n8n CloudでもNODE_OPTIONSは設定できますか?
n8n Cloudでは環境変数をユーザー側で直接編集できないため、この方法は使えません。
クラウド版でメモリ不足が発生する場合は、上位プランへのアップグレードで割り当てリソースを増やすか、本記事のSplit In Batches設計で対応します。
Q3. Codeノードでbinaryデータを扱うとメモリを消費しやすいのはなぜですか?
バイナリデータをBase64文字列に変換すると、元のファイルサイズより約1.3倍にデータ量が膨らむためです。
大きなファイルは以下のように、Base64変換前提のコードを避け、バイナリのまま次のノードに渡す設計に変更します。
// ファイル名: avoid_base64.js
// Base64変換を避け、バイナリ参照のみを保持する
return items.map(item => ({
json: { fileName: item.binary.data.fileName },
binary: item.binary,
}));
Q4. メモリ使用量を監視する方法はありますか?
Codeノード内で以下のコードを実行すると、その時点でのメモリ使用量を数値で取得できます。
// ファイル名: monitor_memory.js
// 現在のNode.jsプロセスのメモリ使用量を確認する
const usage = process.memoryUsage();
return [{
json: {
heapUsedMB: (usage.heapUsed / 1024 / 1024).toFixed(1),
heapTotalMB: (usage.heapTotal / 1024 / 1024).toFixed(1),
}
}];
この値を実行のたびにログ出力し、右肩上がりで増え続けている場合は、該当ワークフローの設計を見直すサインです。
まとめ:Codeノードのメモリエラーは仕組みを知れば怖くない
今回は、n8nのCodeノードで発生する「JavaScript heap out of memory」エラーの原因と解決策を解説しました。
NODE_OPTIONSの調整も有効ですが、根本的にはCodeノードへの依存を減らし、Split In Batchesでデータを分割する設計が最も安定します。
仕組みを理解しておけば、大量データを扱うワークフローでも慌てず対処できるはずです。