n8n Codeノードのメモリエラーを表すイメージ画像

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ノードは、標準ノードと比べてメモリ消費が大きい部類に入ります。データ量が多いワークフローほど、この特性の影響を受けやすくなります。

V8エンジンのメモリ管理イメージ

原因を切り分ける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)ノードで事前にデータを分割します。

具体的な設計手順は以下の通りです。

  1. データ取得ノードの直後にLoop Over Itemsノードを配置する
  2. 「Batch Size」を100〜500件程度に設定する(メモリ状況に応じて調整)
  3. ループの内側にCodeノードを配置し、バッチ単位のデータのみを処理させる
  4. ループの出口を再度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でデータを分割する設計が最も安定します。

仕組みを理解しておけば、大量データを扱うワークフローでも慌てず対処できるはずです。

参考リンク