この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:4
ページ更新者:T
更新日時:2026-06-11 07:12:00

タイトル: 無限ループが検出されました
SEOタイトル: 無限ループ (Maximum execution time exceeded) の原因と対処

この記事の要点
  • PHP: Fatal error: Maximum execution time of 30 seconds exceeded = スクリプト時間制限超過。多くは無限ループ
  • 主な原因: 終了条件のない while / for、再帰の base case 忘れ、配列ポインタを進めない、外部 API ループ
  • 調査: debug_backtrace() / Xdebug ステップ実行 / xhprof で関数別 CPU 時間を測る
  • 緊急回避: set_time_limit(0) で時間制限解除(CLI バッチ向け)。ブラウザ実行では非推奨
  • JavaScript: ブラウザが「ページが応答していません」表示。Node.js は while(true) でイベントループが止まる

エラー内容

PHP

Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/html/app/Service.php on line 42

# または
Fatal error: Maximum execution time of 60 seconds exceeded in ...

# Laravel での例
Symfony\Component\ErrorHandler\Error\FatalError:
  Maximum execution time of 30 seconds exceeded

JavaScript (ブラウザ)

このページは応答していません。
ページを終了するか、引き続き待機します。

# Chrome DevTools コンソール
[Violation] 'click' handler took 5234ms

原因の典型パターン

1. while の終了条件忘れ

// ❌ 終了条件が変わらない
$i = 0;
while ($i < 10) {
    echo $i;
    // $i++ を忘れた!
}

// ✅ 正しい
$i = 0;
while ($i < 10) {
    echo $i;
    $i++;
}

2. 配列ポインタを進めない

// ❌ next() し忘れ
$arr = [1, 2, 3];
reset($arr);
while (($v = current($arr)) !== false) {
    echo $v;
    // next() を忘れた → 永遠に最初の要素
}

// ✅ foreach に書き換える
foreach ($arr as $v) {
    echo $v;
}

3. 再帰の base case 忘れ

// ❌ 終了条件なし
function factorial($n) {
    return $n * factorial($n - 1);   // 永遠に再帰
}

// ✅ base case あり
function factorial($n) {
    if ($n <= 1) return 1;            // ★ 終了条件
    return $n * factorial($n - 1);
}

// 終了条件があっても入力が悪いと無限再帰
factorial(-1);    // -1 * -2 * -3 ... → スタックオーバーフローまで暴走
// → 入力検証も必要
function factorial(int $n): int {
    if ($n < 0) throw new InvalidArgumentException();
    if ($n <= 1) return 1;
    return $n * factorial($n - 1);
}

4. ループ内でループカウンタを変更

// ❌ ループ内で $i をリセット
for ($i = 0; $i < 10; $i++) {
    if (someCondition()) {
        $i = 0;    // ★ 何度も最初に戻る
    }
}

// ✅ break / continue
for ($i = 0; $i < 10; $i++) {
    if (someCondition()) {
        break;
    }
}

5. 浮動小数の比較

// ❌ 浮動小数誤差で終わらない
for ($x = 0.0; $x != 1.0; $x += 0.1) {
    echo $x;
}
// → 0.1 + 0.2 + ... が 1.0 ぴったりにならない(浮動小数の宿命)

// ✅ < で比較 / 誤差許容
for ($x = 0.0; $x < 1.0 - 1e-9; $x += 0.1) {
    echo $x;
}

// ✅ 整数で回す
for ($i = 0; $i < 10; $i++) {
    $x = $i * 0.1;
    echo $x;
}

6. ループ条件の変数を更新しない

// ❌ DB ループで last_id を更新しない
$lastId = 0;
while ($rows = DB::select('SELECT * FROM logs WHERE id > ? LIMIT 1000', [$lastId])) {
    foreach ($rows as $row) {
        // 処理
    }
    // ★ $lastId を更新し忘れ → 同じデータを永遠に取り続ける
}

// ✅
$lastId = 0;
while ($rows = DB::select('SELECT * FROM logs WHERE id > ? ORDER BY id LIMIT 1000', [$lastId])) {
    foreach ($rows as $row) {
        // 処理
    }
    $lastId = end($rows)->id;    // ★ 最大 id を更新
}

原因の調査方法

方法1: エラーログ + デバッグ

// register_tick_function でループ内に挿入
declare(ticks=1);

$count = 0;
function watchdog() {
    global $count;
    if (++$count > 100000) {
        error_log('Possible infinite loop: ' . print_r(debug_backtrace(), true));
        exit(1);
    }
}
register_tick_function('watchdog');

// 怪しいコード実行
problematicCode();

方法2: Xdebug でステップ実行

# Xdebug インストール
pecl install xdebug

# php.ini
[xdebug]
zend_extension=xdebug.so
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=localhost
xdebug.client_port=9003

# VS Code: PHP Debug 拡張で接続
# IDE でブレークポイント設定 → ループ内で何度も止めて状態確認

方法3: xhprof / Tideways で関数別プロファイル

xhprof_enable(XHPROF_FLAGS_CPU);

problematicCode();

$data = xhprof_disable();
include "xhprof_lib/utils/xhprof_lib.php";
include "xhprof_lib/utils/xhprof_runs.php";
$runs = new XHProfRuns_Default();
$runId = $runs->save_run($data, "test");
// → ブラウザ http://example.com/xhprof_html/?run=$runId&source=test
// → 累積時間が異常に多い関数 = 無限ループの主犯

// 簡易版: microtime で囲む
$start = microtime(true);
problematicCode();
$elapsed = microtime(true) - $start;
error_log("elapsed: {$elapsed}s");

対処1: 緊急回避 (時間制限解除)

// ファイル単位で時間制限解除
set_time_limit(0);   // 0 = 無制限

// 1 操作ごとに延長
set_time_limit(60);  // この呼出から 60 秒延長

// CLI は元々制限なし(max_execution_time = 0)
// → ブラウザ実行のみ制限がかかる

// php.ini
max_execution_time = 60       // デフォルト 30
max_input_time = 60
memory_limit = 256M

注意: 時間制限解除は無限ループの隠蔽になります。CLI バッチ等の正当な長時間処理にのみ使用してください。

対処2: ループ自体に上限を入れる

// ✅ 防衛的プログラミング: 上限を設定
$maxIter = 1_000_000;
$i = 0;
while (someCondition()) {
    if (++$i > $maxIter) {
        throw new RuntimeException("possible infinite loop: $i iterations");
    }
    // 処理
}

// 関数: 上限付き再帰
function recurse($n, $depth = 0) {
    if ($depth > 100) {
        throw new RuntimeException('max recursion depth exceeded');
    }
    if ($n <= 0) return;
    recurse($n - 1, $depth + 1);
}

JavaScript の無限ループ

// ❌ ブラウザのメインスレッドを止める
while (true) {
    // タブが応答しなくなる、最終的にブラウザが kill
}

// ❌ Promise 内で await を await し続ける
async function infinite() {
    while (true) {
        await Promise.resolve();   // ★ イベントループに戻らない(Microtask)
    }
}

// ✅ setTimeout でループするとブラウザに制御を返せる
function poll() {
    if (done()) return;
    process();
    setTimeout(poll, 100);    // 次回 100ms 後
}
poll();

// ✅ requestAnimationFrame
function tick() {
    if (done()) return;
    update();
    requestAnimationFrame(tick);
}
tick();

// ✅ Web Worker で別スレッドへ
const worker = new Worker('worker.js');
worker.postMessage({ data });

Node.js の場合

// ❌ イベントループを止める
while (true) {
    // ★ 他のイベント(HTTP, ファイル I/O)が処理されない
}

// ✅ setImmediate / setTimeout でイベントループに譲る
function loop() {
    if (done()) return;
    work();
    setImmediate(loop);
}
loop();

// ✅ async / await + setTimeout
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function run() {
    while (!done()) {
        await work();
        await sleep(10);    // ★ 他処理に譲る
    }
}

// CPU 集約処理は worker_threads
const { Worker } = require('worker_threads');
new Worker('./worker.js');

FAQ

Q: Laravel のジョブが「Maximum execution time exceeded」になる
A: ジョブ用に $timeout プロパティ設定。php artisan queue:work --timeout=300 で 300 秒に。それでも長いなら処理を分割。

Q: PHP-FPM で max_execution_time 効かない
A: nginx / Apache 側のタイムアウト(fastcgi_read_timeout / ProxyTimeout)が先に発火する可能性。両方確認。

Q: 無限ループを止める Ctrl+C が効かない
A: PHP の pcntl_signal() でシグナルハンドラ登録 + declare(ticks=1) を併用。Web 経由は CLI 終了で。