Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Why My Node Process Kept Growing: Field Notes on Heap Snapshots, Module-Scope Caches, and Leaks That Only Appear When Instances Are Reused

2026年10月17日に「Dev.to」が公開したITニュース「Why My Node Process Kept Growing: Field Notes on Heap Snapshots, Module-Scope Caches, and Leaks That Only Appear When Instances Are Reused」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

Node.jsのメモリリークは、リクエスト完了後も残る「モジュールスコープ」の変数(Mapなど)が原因で発生する。強制GC後のメモリ使用量やヒープスナップショット比較で特定。サーバーレス環境でも起こるため、`lru-cache`などで対策し、テストで予防しよう。

ITニュース解説

Node.jsアプリケーションを運用していると、時間が経つにつれてプロセスのメモリ使用量がじわじわと増加し、最終的にアプリケーションのパフォーマンスが低下したり、最悪の場合クラッシュしたりする現象に遭遇することがある。これは多くの場合「メモリリーク」と呼ばれる問題であり、特にシステムエンジニアを目指す初心者にとってはその原因の特定と解決が難しい課題である。このメモリリークは、リクエスト処理が遅くなるような一般的なパフォーマンス問題とは異なり、一度アプリケーションを再起動すると一時的に解決するため、根本原因の特定がさらに困難になる傾向がある。

Node.jsにおけるメモリリークのほとんどは、個々のリクエストハンドラ内で一時的に割り当てられたメモリが解放されないことに起因するものではなく、「モジュールスコープ」で宣言された変数が、リクエストのライフサイクルを超えて参照され続けることによって発生する。モジュールスコープとは、Node.jsのモジュールファイル内で関数の外、つまりグローバルに近い場所で宣言された変数やオブジェクトが格納される領域を指す。ここに作成されたデータは、明示的に削除されない限り、アプリケーションプロセスが終了するまでメモリに残り続ける特性がある。

メモリリークを正確に診断するには、まずメモリ使用状況の計測方法を理解することが重要である。process.memoryUsage()というNode.jsの組み込み関数は、プロセスのメモリ情報を取得できる。この関数が返す値の中で特に重要なのはheapUsedとrssである。heapUsedはV8(Node.jsがJavaScriptを実行するためのエンジン)のヒープ内で現在JavaScriptオブジェクトによって使用されているメモリ量を指す。一方、rss(Resident Set Size)はオペレーティングシステムがプロセスに割り当てた物理メモリの総量であり、JavaScriptオブジェクトだけでなく、Node.js本体やC++モジュールなどが使用するネイティブメモリも含む。そのため、rssが増加しているだけでは必ずしもJavaScriptオブジェクトがリークしているとは断定できず、heapUsedの推移を追うことがより重要になる。さらに正確な計測のためには、Node.jsを--expose-gcオプション付きで起動し、global.gc()を呼び出して強制的にガベージコレクション(不要なメモリを自動的に解放する仕組み)を実行した直後のheapUsedの値を参照することで、JavaScriptオブジェクトが本当に解放されずに残っているかを確認できる。真のメモリリークは、ガベージコレクション後にheapUsedのベースラインが元の値に戻らず、上昇し続ける「上昇する床」として現れることが特徴である。一時的な負荷によるメモリ増加は、ガベージコレクション後にはベースラインに戻る「のこぎり状」のパターンを示す。

具体的なメモリリークの特定には、ヒープスナップショットの解析が非常に有効である。Node.jsプロセスを--heapsnapshot-signal=SIGUSR2オプション付きで起動すると、kill -USR2コマンドを送信するだけで、実行中のプロセスのメモリ状態を記録した.heapsnapshotファイルを生成できる。アプリケーションが正常な状態と、メモリが増加した状態の2つのスナップショットを取得し、これらをChrome DevToolsのメモリパネルにある「Comparison」ビューで比較することで、どのオブジェクトがどれだけ増加したかを詳細に確認できる。特に、オブジェクトがどこから参照されているかを示す「Retainers(保持者)」パネルを追跡することで、リークの根本原因となっているデータ構造やコード箇所を特定することが可能となる。

Node.jsサーバーでよく見られるメモリリークのパターンはいくつか存在する。一つは、リクエストごとに生成されるユニークな識別子(ユーザーID、リクエストID、冪等性キーなど)をキーとしてMapやプレーンなオブジェクトにデータを無制限に追加し続けるパターンである。これは一種のキャッシュとして機能するが、古いエントリが削除されないため、そのMapは際限なく大きくなる。次に、永続的に存在するイベントエミッターに、リクエストごとに新しいイベントリスナーを追加し、そのリスナーが適切に解除されないパターンがある。これはNode.jsからMaxListenersExceededWarningという警告が出ることがあるが、それ以降は無言でリスナー数が増え続けるため注意が必要である。また、setIntervalやsetTimeoutのようなタイマーが、リクエストに関連するオブジェクトをキャプチャしたクロージャ(関数と、それが宣言された環境の変数の組み合わせ)を保持し続けると、そのタイマーが解除されない限り、リクエストオブジェクト全体がメモリに残り続けることになる。さらに、非同期処理間でリクエストコンテキストを伝搬させるためのAsyncLocalStorageが、リクエストオブジェクトを保持し続ける場合や、Bufferやストリームから取得したデータをモジュールレベルの配列に蓄積し続ける場合もリークにつながる。後者の場合、JavaScriptヒープ外のメモリであるexternalやrssが増加し、heapUsedは比較的安定しているため、heapUsedのみを監視していると見落としがちである。

サーバーレス環境、例えばVercelのFluid Computeのような実行モデルでもメモリリークは発生する。サーバーレス関数はリクエストごとに完全に新しい環境で実行されると誤解されがちだが、パフォーマンス最適化のため、一度起動した関数インスタンスは次のリクエストでも再利用されることがある。このインスタンスの再利用という特性により、モジュールスコープで宣言された変数はリクエスト間で状態を保持し続けるため、通常の長期稼働サーバーアプリケーションと同様にメモリリークが発生しうる。これは、データベース接続プールのように、モジュールスコープでシングルトンとして宣言することでリクエスト間で再利用できる機能が意図的に活用されているのと、全く同じメカニズムによって発生する。

モジュールスコープでキャッシュを安全に利用するためには、そのキー空間がトラフィック量以外の要素によって明確に制限されている必要がある。例えば、アプリケーション設定のような固定された小さなキー空間を持つデータや、コンパイル済みの正規表現などは安全にモジュールスコープに配置できる。WeakMapはキーがオブジェクトである場合に、そのキーオブジェクトが他のどこからも参照されなくなると、対応するエントリがガベージコレクションによって自動的に削除されるため、メモリリークのリスクを低減できる。しかし、WeakMapのキーはオブジェクトまたは登録済みシンボルに限定されるため、ユーザーIDのような文字列をキーとするキャッシュには適用できず、lru-cacheのように最大エントリ数や有効期限を設定できるライブラリを使用する必要がある。一般に、ユーザー入力から導出される可能性のあるキーを持つキャッシュには、必ず上限を設定すべきである。

メモリリークを本番環境で問題となる前に発見するためには、開発段階で予防的なテストを導入することが有効である。Node.jsの組み込みテストランナーと--expose-gcオプションを組み合わせて、特定の処理を何度も繰り返し実行し、その前後のheapUsedの増加量を強制ガベージコレクション後に比較するテストを作成できる。このテストでは、heapUsedが完全にゼロ増加であることを期待するのではなく、ある程度の増加(例えば数MB以内)を許容する「予算」を設定することが現実的である。なぜなら、V8はコンパイル済みコードやインターンされた文字列など、ウォームアップ後に正当にメモリを保持する部分があるためである。本番環境での監視においては、コンテナのメモリ制限よりも--max-old-space-sizeを低く設定することで、V8がアウトオブメモリエラーを発生させる前にヒープスナップショットを書き出せるようにすると、クラッシュ時の原因調査に役立つ。さらに、rssとheapUsedの値を継続的にメトリクスとして収集し、数時間にわたる監視期間での「最小値」が上昇している場合にアラートを発する設定が有効である。一時的なメモリのピークはトラフィックの変動によるものであることが多いが、ベースラインとなる最小値が上昇し続けている場合は、メモリリークの明確な兆候と判断できる。

関連コンテンツ

関連IT用語

関連ITニュース