草壁シトヒ
草壁シトヒ SHITOHI.COM
ブログ・SEO
PR 当ページのリンクには広告が含まれています。

astro buildが止まるエラーの原因とCPU0%永久フリーズの対処法

astro buildが止まるエラーの原因とCPU0%永久フリーズの対処法

astro buildの実行中にエラーメッセージも吐かずCPU使用率0.0%のまま停止する主因は、APFSファイルシステムの単一ディレクトリハードリンク上限(65,535個)に達したdistディレクトリに対し、Node.jsのemptyDir(fs.rmSync)が同期深さ優先探索を行ってI/Oキューがデッドロックするためです。

PoW

APFSハードリンク窒息デッドロック・実機障害検証ログ

実機テスト・コード検証済み一次データ

Verified: 2026-09
検証環境 / スタック: macOS APFS / Node.js 22.x / Astro 5.x / 1,000+ ページ本番環境
ハードリンク枯渇閾値
65,535 links
CPUフリーズ率
0.0% (S+)
O(1)復旧時間
< 50ms

キャッシュを削除し、node_modules を再インストールし、設定ファイルを見直しても解決しない場合、問題はソースコードではなくファイルシステムとNode.jsの境界線に存在します。

本記事では、1000ページ超の大規模Astroメディア運用現場で発生した一次実測ログをもとに、Astroビルドがフリーズする4大エラー原因、計算量O(1)で瞬時に脱出するポインタ付け替えハック、そして二度とビルドを停止させない「自律型ビルド防衛Sentinel」の実装コードまでを網羅して解説します。

astro buildが途中で止まる・フリーズする4大エラー原因

Astroの静的ビルド(astro build)が終了せず、コンソールが停止するトラブルには明確な技術的パターンが存在します。闇雲に設定を弄る前に、以下の4つの原因のいずれに該当するかを特定してください。

💡 Astroビルド停止・フリーズの4大原因マトリクス
障害分類停止のタイミングCPU / メモリ挙動主な原因と発生条件
① APFSハードリンク窒息Collecting build info... 直後CPU 0.0% / メモリ正常 / プロセスS+dist 内の再帰リンクが65,535個に達し、fs.rmSync がデッドロック
② Content Layerキャッシュ破損コマンド実行直後(0.5秒以内)CPU 0.0% / ハングアップ.astro/ や node_modules/.astro/ のシリアライズ破損・重複ゴースト
③ リダイレクト定義の衝突ルート解決・ページ生成開始時CPU 100% または 無限待機astro.config.mjs 内の末尾スラッシュ有無(/foo/ と /foo)の二重定義
④ 外部APIフェッチの未解決静的エントリポイント生成中CPU 0.0% / タイムアウト待ちgetStaticPaths 等でのfetchにTimeout/AbortControllerが未設定

特に初見で最も時間を溶かしやすいのが、①の「エラーを一切吐かず、CPU使用率0.0%のまま一生眠り続ける現象」です。次章でそのメカニズムを深層解剖します。

【一次実測】エラーすら出ない「CPU 0.0%」停止の解剖とfs.rmSync

全1035本の大規模MDX記事を抱える本番環境において、npm run build を実行した際、ターミナルは以下の行を出力したまま完全に沈黙しました。

22:31:33 [build] output: "static"
22:31:33 [build] mode: "static"
22:31:33 [build] directory: /.../sites/site-name/dist/
22:31:33 [build] Collecting build info...
22:31:33 [build] ✓ Completed in 616ms.

通常であれば、この直後に Building static entrypoints... というログが出力され、Viteによる静的レンダリングが開始されます。しかし、Completed in 616ms. の行でカーソルの点滅が静止します。

別ターミナルから ps aux で該当プロセスを検査した実測データが以下です。

ps aux | grep "astro.js build"
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
user 85240   0.0  2.3 471074416 384032 s001 S+   10:39PM   0:01.24 node /.../astro/astro.js build

実行時間(TIME)は1.24秒で停止しており、CPU使用率は 0.0%。プロセスステータスは「S+(割り込み可能なスリープ状態)」でした。

Astroコア内部の真犯人:emptyDir() と fs.rmSync

Astro 5のビルドパイプライン内部コード(node_modules/astro/dist/core/build/static-build.js)をトレースすると、ログ停止地点の直前で以下の処理が実行されていることが判明しました。

// static-build.js
if (settings.config?.vite?.build?.emptyOutDir !== false) {
  emptyDir(settings.config.outDir, new Set(".git"));
}

さらに emptyDir の実装(core/fs/index.js)を確認すると、前回のビルド成果物を一掃するために Node.js 標準の同期削除関数 fs.rmSync が呼ばれています。

// core/fs/index.js
function emptyDir(_dir, skip) {
  const dir = fileURLToPath(_dir);
  if (!fs.existsSync(dir)) return void 0;
  for (const file of fs.readdirSync(dir)) {
    if (skip?.has(file)) continue;
    const p = path.resolve(dir, file);
    const rmOptions = { recursive: true, force: true, maxRetries: 3 };
    try {
      fs.rmSync(p, rmOptions);
    } catch (er) {
      // 例外処理
    }
  }
}

限界値「ハードリンク65,535個」の衝突

このとき、停止していた dist ディレクトリの詳細ステータスを ls -la で検査した結果です。

drwx------  65535 user  staff  2097120 Sep 20 22:24 target-guide

特定のディレクトリのハードリンク数が 65,535個 に達していました。これは16ビット符号なし整数の最大値であり、macOSのファイルシステム(APFS/HFS+)における単一ディレクトリのハードリンク上限値です。

過去のビルドや画像変換の異常によって、内部に自己参照的な循環リンクや数十万件のゴーストフォルダが増殖しており、Node.jsのメインスレッドがその巨大なツリー構造を同期的に走査しようとした結果、OSのI/Oキューが完全にデッドロックし、CPU 0.0%の窒息状態に陥っていたのです。

rm -rfは厳禁!0.001秒で突破する「同一ボリューム内ポインタ付け替え(mv)」

この状態に直面した際、ターミナルから手動で rm -rf dist を実行したり、Finderからゴミ箱へ移動させようとするのは絶対に避けてください。

なぜ手動の rm -rf でもMacごと道連れフリーズするのか?

UNIXの標準コマンド rm -rf は、対象ディレクトリツリーのすべてのinodeを走査し、1件ずつ unlink システムコールを発行します。

ハードリンク上限(65535)に達したモンスターディレクトリに対して rm -rf を実行すると、数十万回のメタデータ変更がディスクへ集中発行されます。その結果、APFSのメタデータ排他ロック(カーネルレベルのロック)が長時間保持され、ターミナルだけでなくFinderや他のアプリもディスクI/O待ちでブロックされ、OS全体が道連れフリーズを起こします。

解決策:同一ボリューム内での名前変更(mv)

このファイルシステムの底なし沼を突破する唯一の解法は、削除するのではなく同一ボリューム内での名前変更(mv)を行うことです。

# distを瞬時に別名へ隔離し、即座に空のdistを配備する
mv dist .dist_trash && mkdir dist

UNIXファイルシステムにおいて、同一ボリューム(同一パーティション)内での mv コマンドは、ファイル実体や配下のツリーを走査しません。親ディレクトリのエントリにおいて「ポインタ(inode番号の参照先)を1箇所書き換えるだけ」で終了します。

配下にファイルが1個あろうが65,535個あろうが、要する計算量は O(1)。0.001秒で処理が完了します。

隔離した .dist_trash ディレクトリは、ビルドが完了した後にバックグラウンドで安全に消去させます。

# シェルをブロックさせず、バックグラウンドで低負荷消去
nohup rm -rf .dist_trash >/dev/null 2>&1 &

【コピペ配備】二度とビルドを止めない「自律型ビルド防衛Sentinel」

トラブルを解決しても、人間の手動運用に頼っていてはいつか再発します。プロジェクトの健全性を担保するためには、「コードによる物理的制約」をビルド前に自動介入させるのが鉄則です。

以下のスクリプトをプロジェクトの監査パイプライン(例: scripts/audit_and_fix_quality.py)の最上流に組み込んでください。

# !/usr/bin/env python3
import os
import re
import shutil
import subprocess
import time

def pre_build_sentinel(base_dir: str):
    """
    【自律型ビルド防衛センチネル (Pre-build Health Sentinel)】
    1. 古いスタック/ゾンビプロセスの自動検死 & 強制終了
    2. dist & .astro の健康診断(ハードリンク過密・破損シリアライズの自動隔離 & 瞬間クリーン配備)
    3. astro.config.mjs のリダイレクト重複・衝突の自動修復
    """
    site_name = os.path.basename(base_dir)
    my_pid = os.getpid()

    # 1. ゾンビプロセスの検死 & 一掃
    try:
        ps_out = subprocess.check_output(["ps", "-eo", "pid,command"], text=True)
        for line in ps_out.strip().split("\n"):
            if site_name in line and ("astro/astro.js" in line or "esbuild" in line):
                parts = line.strip().split(None, 1)
                if parts and parts[0].isdigit():
                    pid = int(parts[0])
                    if pid != my_pid and pid != os.getppid():
                        try:
                            os.kill(pid, 9)
                            print(f"🛡️ [SENTINEL] スタックしていた古いプロセス (PID: {pid}) を安全に終了させました。")
                        except ProcessLookupError:
                            pass
    except Exception:
        pass

    # 2. dist & .astro の健康診断と瞬時自浄
    dist_dir = os.path.join(base_dir, "dist")
    astro_cache = os.path.join(base_dir, ".astro")

    if os.path.exists(dist_dir):
        needs_reset = False
        try:
            for entry in os.scandir(dist_dir):
                if entry.is_dir(follow_symlinks=False):
                    stat = entry.stat(follow_symlinks=False)
                    # ファイルシステム上限(65535)に近い異常なハードリンク数を検知
                    if stat.st_nlink >= 30000:
                        needs_reset = True
                        print(f"⚠️ [SENTINEL] dist/{entry.name} に異常なハードリンク数 ({stat.st_nlink}) を検知しました。")
                        break
        except Exception:
            needs_reset = True

        if needs_reset:
            print("🛡️ [SENTINEL] dist が破損または異常肥大化しているため、瞬間隔離&初期化します...")
            trash_name = f".dist_trash_{int(time.time())}"
            trash_path = os.path.join(base_dir, trash_name)
            try:
                os.rename(dist_dir, trash_path)
                os.makedirs(dist_dir, exist_ok=True)
                subprocess.Popen(["rm", "-rf", trash_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                print("✨ [SENTINEL] dist をクリーンな状態に瞬間初期化しました。")
            except Exception as e:
                print(f"⚠️ [SENTINEL] dist 初期化警告: {e}")

    # 3. astro.config.mjs のリダイレクト重複・衝突の自動修復
    config_path = os.path.join(base_dir, "astro.config.mjs")
    if os.path.exists(config_path):
        try:
            with open(config_path, "r", encoding="utf-8") as f:
                cfg_content = f.read()

            m = re.search(r'(redirects:\s*\{)(.*?)\n\s*\},', cfg_content, re.DOTALL)
            if m:
                body = m.group(2)
                lines = body.split("\n")
                seen_routes = set()
                new_lines = []
                modified_redirects = False

                for l in lines:
                    r_match = re.search(r"^\s*['\"]([^'\"]+)['\"]\s*:\s*(.+)$", l)
                    if r_match:
                        route = r_match.group(1)
                        norm_route = route.rstrip("/") + "/"
                        if norm_route in seen_routes:
                            modified_redirects = True
                            continue
                        seen_routes.add(norm_route)
                        new_lines.append(l)
                    else:
                        new_lines.append(l)

                if modified_redirects:
                    new_body = "\n".join(new_lines)
                    new_cfg = cfg_content[:m.start(2)] + new_body + cfg_content[m.end(2):]
                    with open(config_path, "w", encoding="utf-8") as f:
                        f.write(new_cfg)
                    print("🛡️ [SENTINEL] astro.config.mjs 内のリダイレクト重複を自動修復しました。")
        except Exception as e:
            print(f"⚠️ [SENTINEL] astro.config.mjs 検査エラー: {e}")

実測検証:1075ページのビルドが50秒で完走

このセンチネルスクリプトを package.json のビルドパイプライン直前に挟んだ結果、全1035記事・1075ページの生成処理が、わずか50.21秒でスタック0秒の完全完走を果たすようになりました。

{
  "scripts": {
    "prebuild": "python3 scripts/audit_and_fix_quality.py",
    "build": "npm run prebuild && astro build"
  }
}

astro buildエラー・停止に関するよくある質問(FAQ)

❓ よくある質問(Q&A・FAQ)
Official Q&A
Q GitHub ActionsやCloudflare Pages等のCI環境でもこのハードリンク問題は起きますか?
A
CI環境が毎回クリーンな仮想マシン(コンテナ)を立ち上げるエフェメラル環境であれば、前回のdistが存在しないためハードリンク問題は起きません。ただし、Actionsのキャッシュ機能(actions/cache等)でdistや.astroを丸ごとキャッシュしている場合、破損したシリアライズやゴーストファイルがリストアされてCIジョブがタイムアウトまでフリーズする事故が発生します。distはキャッシュ対象から必ず除外してください。
Q Astroのキャッシュ(.astro/)を安全に手動リセットするコマンドは?
A
ターミナルから『rm -rf .astro node_modules/.astro』を実行してください。Astro 5のContent Layerでは、コレクションの同期メタデータが『.astro/data-store.json』に格納されています。このファイルが破損した場合は、キャッシュを全消去して『npx astro sync』を実行することで再生成できます。
Q Windows(NTFS)環境でも同様のビルド停止は発生しますか?
A
Windows環境ではハードリンク上限の前に、ファイルパスが260文字を超えることによる『ENAMETOOLONG』エラーや、Node.jsのファイルロックによる『EBUSY / EPERM』エラーとして表面化します。Windowsの場合も『rename dist dist_trash && rmdir /s /q dist_trash』のようにリネームによる隔離が有効です。
Q astro.config.mjsのリダイレクト重複でなぜビルドが止まるのですか?
A
Astroの静的ルーティング生成時、同一ルートに対して末尾スラッシュの有無(例: '/article' と '/article/')が両方定義されていると、ルーター解決エンジンが循環参照に陥るか、出力先のHTMLファイル書き込み時に競合エラーを起こしてイベントループがデッドロックするためです。

まとめ:インフラとOSカーネルを理解してビルドを最速化する

モダンな静的サイトジェネレーター(SSG)がどれほど進化しようとも、最終的に実行されるのはハードウェアとOSカーネルの上です。

「Astroが遅い」「Viteがバグった」と表層の不満で片付けるのではなく、システムコールの挙動やファイルシステムの特性(inodeとハードリンクの限界値)を理解することこそが、予期せぬトラブルから最速で脱出する唯一の武器になります。

Astroのビルドが謎の停止に見舞われた際は、ぜひ本記事のポインタ付け替えハック(mv)と自動防衛Sentinelを活用してください。

※本検証の泥臭い深夜のデバッグ格闘記・リアルタイムドキュメントは、草壁シトヒ公式noteの解説エッセイ でも公開しています。