pnpm generate が exit 134 で落ちた日——ヒープOOMと検証スクリプト偽陽性の二段落ち
pnpm generate が exit 134 で落ちた日——ヒープOOMと検証スクリプト偽陽性の二段落ち
結論
このサイト(Nuxt 3 / SSG / Cloudflare Pages)のデプロイが同じ朝に2回、別々の理由で落ちた。
- 1回目: exit 134。V8のヒープが上限4GBに到達したOOM(out of memory)だった。
NODE_OPTIONS=--max-old-space-sizeを4096から8192に引き上げて解決した - 2回目: ビルド時検証スクリプトの偽陽性。prerenderは完走したのに、postgenerateの
verify-blog-payload.mjsが「当月の記事16本がブログページに載っていない」と報告して exit 1。実装側が持つ除外リストに検証側が追随していないのが原因で、基準を揃えて解決した
exit 134 を見たらビルドログの最終行ではなく、その少し上にある Last few GCs を探す。
そこに Reached heap limit があれば、コードのバグではなくメモリ上限の問題である。
何が起きたか
タイムラインは次のとおり。
| 時刻 | 出来事 |
|---|---|
| 08:23 | デプロイスクリプト実行。Nuxt Setup・Client Build・Server Build まで成功 |
| 08:31 | Prerenderer 初期化直後に exit 134 で異常終了 |
| 08:46 | ヒープ上限を8GBに引き上げて再実行 |
| 08:57 | prerender 2,457ルート完走。ただし postgenerate の検証で exit 1 |
| 09:05 | 検証スクリプトの偽陽性を修正し、全検証グリーン |
エラー1: exit 134 = JavaScript heap out of memory
ログの読み方
ビルドログの末尾はこうなっていた。
[nitro] ℹ Initializing prerenderer
<--- Last few GCs --->
Mark-Compact 4033.7 (4135.8) -> 4031.0 (4133.8) MB, ... allocation failure;
Mark-Compact 4046.7 (4133.8) -> 4046.0 (4165.8) MB, ... allocation failure;
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
ELIFECYCLE Command failed with exit code 134.
読み取れることは3つある。
- Client Build(5,908モジュール)と Server Build(4,081モジュール)は成功しており、コンパイルの問題ではない
- ヒープ使用量が 4,033〜4,046MB で頭打ちになっている。
--max-old-space-size=4096の設定値とぴったり一致する - exit 134 は「128 + 6(SIGABRT)」で、V8が自らプロセスを中断したときの終了コードである
なぜ上限が4096だったのか
この4096という値には歴史がある。
2026年1月、GitHub Actions(メモリ7GBのランナー)で pnpm generate がOOMを連発し、試行錯誤の末に NODE_OPTIONS=--max-old-space-size=4096 を package.json に直接埋め込んで一時しのぎした。
最終的にはActionsでのビルドを諦め、ローカルビルド+wrangler直接デプロイに移行している(経緯は SSGスケーラビリティ課題とローカルデプロイ移行 に書いた)。
当時はこの値で通っていた。 しかし半年でコンテンツと学習ツールページが増え、prerenderルートは2,457件になった。 今朝そこに security-cases の詳細321ルートを足したことが引き金になり、ローカル(物理メモリ32GB)でも4GBの壁を踏み抜いた。
修正
package.json の generate スクリプトを1か所変えるだけで済んだ。
- "generate": "cross-env NODE_OPTIONS=--max-old-space-size=4096 nuxt generate",
+ "generate": "cross-env NODE_OPTIONS=--max-old-space-size=8192 nuxt generate",
物理メモリ32GBの手元マシンなら8GBヒープは問題なく確保できる。 再実行でprerenderは完走した。
なお「ビルド(transform)は通るのにprerenderer初期化で死ぬ」のはこの障害の典型パターンである。 Viteのビルドで確保したメモリを抱えたまま、Nitroがサーバーバンドルとルート一覧をメモリに展開する瞬間がピークになる。
エラー2: verify-blog-payload の偽陽性
症状
ヒープを増やした再ビルドでprerenderは完走したが、今度はpostgenerateの検証スクリプトが落ちた。
[verify-blog-payload] calendar month 2026-07: 32/48 content articles linked in HTML
✖ blog payload checks failed:
- 16 article(s) in calendar month 2026-07 are missing from /blog page HTML
(e.g. 2026-07-02 /genai-ideas/analysis-stock, ...)
このスクリプトは、過去に本番で起きた「payloadシリアライズで記事一覧がnullに化け、ブログカレンダーが空になる」事故の再発を検知するために置いてある。 「content/ にある当月記事が /blog ページのHTMLからリンクされているか」を突き合わせる仕組みである。
原因: 実装と検証が同じ基準を別々に持っていた
missing と報告された16本は、すべて /genai-ideas/ 配下の連載記事だった。
- ブログ一覧を組み立てる
useBlogArticles.tsは、/takken/(教材)と/genai-ideas/(連載)を意図的に一覧から除外している。どちらも専用のハブページから読む独立シリーズという設計で、ブログカレンダーに出ないのは仕様である - 一方、検証スクリプト側の除外リストは
/takken/だけだった。/genai-ideas/の除外を実装に足したとき、検証側の更新が漏れていた
この連載の記事16本が公開されたのは7月2〜3日だった。 それらを含む最初のビルドが今朝だったため、今日まで誰もこの地雷を踏まなかった。
修正
検証スクリプト側の除外リストを実装と同じにした。
// scripts/verify-blog-payload.mjs
const STANDALONE_PATH_PREFIXES = ['/takken/', '/genai-ideas/']
const isStandalonePath = (p) => STANDALONE_PATH_PREFIXES.some((prefix) => p.startsWith(prefix))
あわせて、useBlogArticles.ts 側の除外フィルタに「ここを増減したら verify-blog-payload.mjs も揃えること。揃えないと generate が偽陽性で落ちる」というコメントを入れ、双方から相手を指すようにした。
同じ基準を2か所で持つ構造は、片方だけが更新されていつか必ず食い違う。 本来は定義を1か所にまとめるのが筋だが、アプリ側はTypeScript、検証側は素のNodeスクリプトで実行環境が違うため、今回は相互ポインタコメントで運用することにした。
おまけ: パイプは exit code を隠す
再ビルドを pnpm generate 2>&1 | tail -60 の形で走らせていたため、シェルに返る終了コードが tail の 0 になり、監視上は「成功」に見えていた。
ログ本文を読んで、実際はexit 1で落ちていたことに気づいた。
ビルドコマンドの成否を終了コードで判定するなら、パイプで包まないか、bashなら set -o pipefail を先頭に置く。
再発防止チェックリスト
- exit 134 を見たら
Last few GCsを探す(Reached heap limitならヒープ上限の問題) - prerenderルート数が増えたらメモリのピークも伸びる前提でヒープ上限を見直す(今回4096→8192)
- 実装とビルド時検証が同じリストを二重に持つ箇所には、双方向のポインタコメントを書く
- ビルドコマンドをパイプで包むときは
pipefailを意識する - ルート数がさらに増えて8GBに近づいたら、prerenderの分割やルート削減を検討する