[{"data":1,"prerenderedAt":436},["ShallowReactive",2],{"content-/generate-oom-and-verify-false-positive":3,"all-pages-for-dir":434,"og-image-/generate-oom-and-verify-false-positive":435},{"id":4,"title":5,"body":6,"category":416,"description":417,"extension":418,"meta":419,"navigation":359,"ogImage":420,"path":421,"project_name":420,"published":422,"publishedAt":423,"seo":424,"stem":425,"tags":426,"todo":432,"unpublished":422,"updatedAt":423,"__hash__":433},"pages/2026-07/2026-07-04/generate-oom-and-verify-false-positive.md","pnpm generate が exit 134 で落ちた日——ヒープOOMと検証スクリプト偽陽性の二段落ち",{"type":7,"value":8,"toc":400},"minimark",[9,13,17,21,47,58,61,64,124,128,132,135,145,148,167,170,187,190,193,196,217,220,223,227,230,233,239,242,246,253,282,285,288,291,314,320,323,327,338,345,348,396],[10,11,5],"h1",{"id":12},"pnpm-generate-が-exit-134-で落ちた日ヒープoomと検証スクリプト偽陽性の二段落ち",[14,15,16],"h2",{"id":16},"結論",[18,19,20],"p",{},"このサイト（Nuxt 3 / SSG / Cloudflare Pages）のデプロイが同じ朝に2回、別々の理由で落ちた。",[22,23,24,37],"ol",{},[25,26,27,31,32,36],"li",{},[28,29,30],"strong",{},"1回目: exit 134","。V8のヒープが上限4GBに到達したOOM（out of memory）だった。",[33,34,35],"code",{},"NODE_OPTIONS=--max-old-space-size"," を4096から8192に引き上げて解決した",[25,38,39,42,43,46],{},[28,40,41],{},"2回目: ビルド時検証スクリプトの偽陽性","。prerenderは完走したのに、postgenerateの ",[33,44,45],{},"verify-blog-payload.mjs"," が「当月の記事16本がブログページに載っていない」と報告して exit 1。実装側が持つ除外リストに検証側が追随していないのが原因で、基準を揃えて解決した",[18,48,49,50,53,54,57],{},"exit 134 を見たらビルドログの最終行ではなく、その少し上にある ",[33,51,52],{},"Last few GCs"," を探す。\nそこに ",[33,55,56],{},"Reached heap limit"," があれば、コードのバグではなくメモリ上限の問題である。",[14,59,60],{"id":60},"何が起きたか",[18,62,63],{},"タイムラインは次のとおり。",[65,66,67,80],"table",{},[68,69,70],"thead",{},[71,72,73,77],"tr",{},[74,75,76],"th",{},"時刻",[74,78,79],{},"出来事",[81,82,83,92,100,108,116],"tbody",{},[71,84,85,89],{},[86,87,88],"td",{},"08:23",[86,90,91],{},"デプロイスクリプト実行。Nuxt Setup・Client Build・Server Build まで成功",[71,93,94,97],{},[86,95,96],{},"08:31",[86,98,99],{},"Prerenderer 初期化直後に exit 134 で異常終了",[71,101,102,105],{},[86,103,104],{},"08:46",[86,106,107],{},"ヒープ上限を8GBに引き上げて再実行",[71,109,110,113],{},[86,111,112],{},"08:57",[86,114,115],{},"prerender 2,457ルート完走。ただし postgenerate の検証で exit 1",[71,117,118,121],{},[86,119,120],{},"09:05",[86,122,123],{},"検証スクリプトの偽陽性を修正し、全検証グリーン",[14,125,127],{"id":126},"エラー1-exit-134-javascript-heap-out-of-memory","エラー1: exit 134 = JavaScript heap out of memory",[129,130,131],"h3",{"id":131},"ログの読み方",[18,133,134],{},"ビルドログの末尾はこうなっていた。",[136,137,142],"pre",{"className":138,"code":140,"language":141},[139],"language-text","[nitro] ℹ Initializing prerenderer\n\n\u003C--- Last few GCs --->\nMark-Compact 4033.7 (4135.8) -> 4031.0 (4133.8) MB, ... allocation failure;\nMark-Compact 4046.7 (4133.8) -> 4046.0 (4165.8) MB, ... allocation failure;\n\nFATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory\nELIFECYCLE  Command failed with exit code 134.\n","text",[33,143,140],{"__ignoreMap":144},"",[18,146,147],{},"読み取れることは3つある。",[149,150,151,157,164],"ul",{},[25,152,153,154],{},"Client Build（5,908モジュール）と Server Build（4,081モジュール）は成功しており、",[28,155,156],{},"コンパイルの問題ではない",[25,158,159,160,163],{},"ヒープ使用量が 4,033〜4,046MB で頭打ちになっている。",[33,161,162],{},"--max-old-space-size=4096"," の設定値とぴったり一致する",[25,165,166],{},"exit 134 は「128 + 6（SIGABRT）」で、V8が自らプロセスを中断したときの終了コードである",[129,168,169],{"id":169},"なぜ上限が4096だったのか",[18,171,172,173,176,177,180,181,186],{},"この4096という値には歴史がある。\n2026年1月、GitHub Actions（メモリ7GBのランナー）で ",[33,174,175],{},"pnpm generate"," がOOMを連発し、試行錯誤の末に ",[33,178,179],{},"NODE_OPTIONS=--max-old-space-size=4096"," を package.json に直接埋め込んで一時しのぎした。\n最終的にはActionsでのビルドを諦め、ローカルビルド＋wrangler直接デプロイに移行している（経緯は ",[182,183,185],"a",{"href":184},"/ssg-scalability-challenges","SSGスケーラビリティ課題とローカルデプロイ移行"," に書いた）。",[18,188,189],{},"当時はこの値で通っていた。\nしかし半年でコンテンツと学習ツールページが増え、prerenderルートは2,457件になった。\n今朝そこに security-cases の詳細321ルートを足したことが引き金になり、ローカル（物理メモリ32GB）でも4GBの壁を踏み抜いた。",[129,191,192],{"id":192},"修正",[18,194,195],{},"package.json の generate スクリプトを1か所変えるだけで済んだ。",[136,197,201],{"className":198,"code":199,"language":200,"meta":144,"style":144},"language-diff shiki shiki-themes vitesse-light vitesse-light","- \"generate\": \"cross-env NODE_OPTIONS=--max-old-space-size=4096 nuxt generate\",\n+ \"generate\": \"cross-env NODE_OPTIONS=--max-old-space-size=8192 nuxt generate\",\n","diff",[33,202,203,211],{"__ignoreMap":144},[204,205,208],"span",{"class":206,"line":207},"line",1,[204,209,210],{},"- \"generate\": \"cross-env NODE_OPTIONS=--max-old-space-size=4096 nuxt generate\",\n",[204,212,214],{"class":206,"line":213},2,[204,215,216],{},"+ \"generate\": \"cross-env NODE_OPTIONS=--max-old-space-size=8192 nuxt generate\",\n",[18,218,219],{},"物理メモリ32GBの手元マシンなら8GBヒープは問題なく確保できる。\n再実行でprerenderは完走した。",[18,221,222],{},"なお「ビルド（transform）は通るのにprerenderer初期化で死ぬ」のはこの障害の典型パターンである。\nViteのビルドで確保したメモリを抱えたまま、Nitroがサーバーバンドルとルート一覧をメモリに展開する瞬間がピークになる。",[14,224,226],{"id":225},"エラー2-verify-blog-payload-の偽陽性","エラー2: verify-blog-payload の偽陽性",[129,228,229],{"id":229},"症状",[18,231,232],{},"ヒープを増やした再ビルドでprerenderは完走したが、今度はpostgenerateの検証スクリプトが落ちた。",[136,234,237],{"className":235,"code":236,"language":141},[139],"[verify-blog-payload] calendar month 2026-07: 32/48 content articles linked in HTML\n✖ blog payload checks failed:\n  - 16 article(s) in calendar month 2026-07 are missing from /blog page HTML\n    (e.g. 2026-07-02 /genai-ideas/analysis-stock, ...)\n",[33,238,236],{"__ignoreMap":144},[18,240,241],{},"このスクリプトは、過去に本番で起きた「payloadシリアライズで記事一覧がnullに化け、ブログカレンダーが空になる」事故の再発を検知するために置いてある。\n「content/ にある当月記事が /blog ページのHTMLからリンクされているか」を突き合わせる仕組みである。",[129,243,245],{"id":244},"原因-実装と検証が同じ基準を別々に持っていた","原因: 実装と検証が同じ基準を別々に持っていた",[18,247,248,249,252],{},"missing と報告された16本は、すべて ",[33,250,251],{},"/genai-ideas/"," 配下の連載記事だった。",[149,254,255,273],{},[25,256,257,258,261,262,265,266,268,269,272],{},"ブログ一覧を組み立てる ",[33,259,260],{},"useBlogArticles.ts"," は、",[33,263,264],{},"/takken/","（教材）と ",[33,267,251],{},"（連載）を",[28,270,271],{},"意図的に一覧から除外","している。どちらも専用のハブページから読む独立シリーズという設計で、ブログカレンダーに出ないのは仕様である",[25,274,275,276,278,279,281],{},"一方、検証スクリプト側の除外リストは ",[33,277,264],{}," だけだった。",[33,280,251],{}," の除外を実装に足したとき、検証側の更新が漏れていた",[18,283,284],{},"この連載の記事16本が公開されたのは7月2〜3日だった。\nそれらを含む最初のビルドが今朝だったため、今日まで誰もこの地雷を踏まなかった。",[129,286,192],{"id":287},"修正-1",[18,289,290],{},"検証スクリプト側の除外リストを実装と同じにした。",[136,292,296],{"className":293,"code":294,"language":295,"meta":144,"style":144},"language-js shiki shiki-themes vitesse-light vitesse-light","// scripts/verify-blog-payload.mjs\nconst STANDALONE_PATH_PREFIXES = ['/takken/', '/genai-ideas/']\nconst isStandalonePath = (p) => STANDALONE_PATH_PREFIXES.some((prefix) => p.startsWith(prefix))\n","js",[33,297,298,303,308],{"__ignoreMap":144},[204,299,300],{"class":206,"line":207},[204,301,302],{},"// scripts/verify-blog-payload.mjs\n",[204,304,305],{"class":206,"line":213},[204,306,307],{},"const STANDALONE_PATH_PREFIXES = ['/takken/', '/genai-ideas/']\n",[204,309,311],{"class":206,"line":310},3,[204,312,313],{},"const isStandalonePath = (p) => STANDALONE_PATH_PREFIXES.some((prefix) => p.startsWith(prefix))\n",[18,315,316,317,319],{},"あわせて、",[33,318,260],{}," 側の除外フィルタに「ここを増減したら verify-blog-payload.mjs も揃えること。揃えないと generate が偽陽性で落ちる」というコメントを入れ、双方から相手を指すようにした。",[18,321,322],{},"同じ基準を2か所で持つ構造は、片方だけが更新されていつか必ず食い違う。\n本来は定義を1か所にまとめるのが筋だが、アプリ側はTypeScript、検証側は素のNodeスクリプトで実行環境が違うため、今回は相互ポインタコメントで運用することにした。",[14,324,326],{"id":325},"おまけ-パイプは-exit-code-を隠す","おまけ: パイプは exit code を隠す",[18,328,329,330,333,334,337],{},"再ビルドを ",[33,331,332],{},"pnpm generate 2>&1 | tail -60"," の形で走らせていたため、シェルに返る終了コードが ",[33,335,336],{},"tail"," の 0 になり、監視上は「成功」に見えていた。\nログ本文を読んで、実際はexit 1で落ちていたことに気づいた。",[18,339,340,341,344],{},"ビルドコマンドの成否を終了コードで判定するなら、パイプで包まないか、bashなら ",[33,342,343],{},"set -o pipefail"," を先頭に置く。",[14,346,347],{"id":347},"再発防止チェックリスト",[149,349,352,368,374,380,390],{"className":350},[351],"contains-task-list",[25,353,356,361,362,364,365,367],{"className":354},[355],"task-list-item",[357,358],"input",{"checked":359,"disabled":359,"type":360},true,"checkbox"," exit 134 を見たら ",[33,363,52],{}," を探す（",[33,366,56],{}," ならヒープ上限の問題）",[25,369,371,373],{"className":370},[355],[357,372],{"checked":359,"disabled":359,"type":360}," prerenderルート数が増えたらメモリのピークも伸びる前提でヒープ上限を見直す（今回4096→8192）",[25,375,377,379],{"className":376},[355],[357,378],{"checked":359,"disabled":359,"type":360}," 実装とビルド時検証が同じリストを二重に持つ箇所には、双方向のポインタコメントを書く",[25,381,383,385,386,389],{"className":382},[355],[357,384],{"checked":359,"disabled":359,"type":360}," ビルドコマンドをパイプで包むときは ",[33,387,388],{},"pipefail"," を意識する",[25,391,393,395],{"className":392},[355],[357,394],{"disabled":359,"type":360}," ルート数がさらに増えて8GBに近づいたら、prerenderの分割やルート削減を検討する",[397,398,399],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":144,"searchDepth":213,"depth":213,"links":401},[402,403,404,409,414,415],{"id":16,"depth":213,"text":16},{"id":60,"depth":213,"text":60},{"id":126,"depth":213,"text":127,"children":405},[406,407,408],{"id":131,"depth":310,"text":131},{"id":169,"depth":310,"text":169},{"id":192,"depth":310,"text":192},{"id":225,"depth":213,"text":226,"children":410},[411,412,413],{"id":229,"depth":310,"text":229},{"id":244,"depth":310,"text":245},{"id":287,"depth":310,"text":192},{"id":325,"depth":213,"text":326},{"id":347,"depth":213,"text":347},"dev","SSGビルドが同じ朝に2回、別々の理由で落ちた記録。1回目はV8ヒープ4GB上限到達のOOM（exit 134）、2回目はビルド時検証スクリプトの除外リスト更新漏れによる偽陽性。それぞれの切り分け手順と修正、再発防止策をログ付きで残す。","md",{},null,"/generate-oom-and-verify-false-positive",false,"2026-07-04T00:00:00.000Z",{"title":5,"description":417},"2026-07/2026-07-04/generate-oom-and-verify-false-positive",[427,428,429,430,431],"Nuxt","SSG","Node.js","Cloudflare Pages","トラブルシューティング","memo","-OMYNLYi04FHx43PFayKBI4m-ySDTQ7LNbLMOPisW6Q",[],"https://log.eurekapu.com/og/blog/generate-oom-and-verify-false-positive.png?v=2026-07-04T00%3A00%3A00.000Z&title=pnpm%20generate%20%E3%81%8C%20exit%20134%20%E3%81%A7%E8%90%BD%E3%81%A1%E3%81%9F%E6%97%A5%E2%80%94%E2%80%94%E3%83%92%E3%83%BC%E3%83%97OOM%E3%81%A8%E6%A4%9C%E8%A8%BC%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E5%81%BD%E9%99%BD%E6%80%A7%E3%81%AE%E4%BA%8C%E6%AE%B5%E8%90%BD%E3%81%A1&author=Kei%20Komatsu&sig=ffe4a95cdc1039d6",1783124621334]