バックエンド 2026.10.16

PHPバッチ処理が途中で止まる原因と対処法

約4分で読めます

タイムアウト・メモリ上限・例外握りつぶし——PHPバッチ処理が黙って止まる3大原因を実案件事例をもとに解説。ログの読み方から修正コードまで実践的に紹介します。

「バッチが終わってないのに、エラーも出ていない」

こんな経験はありませんか?

  • 夜間バッチを仕掛けたのに、翌朝見たら途中のデータしか処理されていない
  • cronのログには「正常終了」と書いてあるのに、明らかに件数が足りない
  • 手動で実行すると動くのに、定期実行だと止まる

原因を調べようにも、エラーメッセージが一切出ていない——これがPHPバッチ処理の「静かな失敗」です。Webリクエストと違い、バッチ処理はユーザーの目に触れないため、問題が発覚するまでに時間がかかります。しかも、止まり方によって原因が異なるため、正しく見分けないと見当違いの対処をしてしまうことになります。

本記事では、PHPバッチ処理が途中で止まる主な3つの原因——タイムアウト・メモリ上限・例外の握りつぶし——を、実案件での事例をもとに解説します。「どれが原因か判断できない」という状態から脱するための実践的な診断手順と修正コードを紹介しますので、ぜひ最後まで読んでください。


なぜPHPバッチは「黙って止まる」のか

PHPはもともとWebリクエストを処理するために設計された言語です。そのため、デフォルトの設定がWebアプリケーションに最適化されており、長時間・大量データを扱うバッチ処理には向いていない側面があります。

具体的には、以下の3つのデフォルト制約が問題になります。

弊社でこれまで対応してきた案件を振り返ると、タイムアウトが最多で約45%、次いでメモリ上限が35%、例外の握りつぶしが20%という感覚値です。ただし厄介なのは、これらが複合して起きているケースも少なくない点です。


バックエンド開発でお困りですか?

API設計・DB最適化・システム構築など、ご相談ください

無料で相談する

原因ごとの見分け方と修正手順

① タイムアウト——「正常終了なのに未処理データが残る」

PHPの max_execution_time は、デフォルトで 30秒 に設定されています。Webサーバー経由で実行するバッチはこの制限に引っかかります。一方、CLIから実行した場合は0(無制限)になりますが、Laravelのスケジューラやcronで呼び出す際の設定ミスで30秒制限が残るケースがあります。

診断方法: エラーログに以下のようなメッセージがあればタイムアウトです。

Fatal error: Maximum execution time of 30 seconds exceeded

ただし、この行がログに残らないケースもあります。そのときは、処理の冒頭と末尾にログを出力して「どこまで到達したか」を確認します。

<?php
// バッチ冒頭でタイムアウト上限を明示的に解除
set_time_limit(0);
ini_set('max_execution_time', 0);

$logger->info('バッチ開始', ['time' => date('Y-m-d H:i:s')]);

foreach ($records as $record) {
    // 処理ごとに進捗ログを出力
    $logger->info('処理中', ['id' => $record->id]);
    processRecord($record);
}

$logger->info('バッチ完了', ['time' => date('Y-m-d H:i:s')]);

あるECサイトクライアントの事例では、毎晩実行している在庫同期バッチが「1,200件中800件で止まる」という問題がありました。ログを仕込んだところ、正確に実行から30秒後にプロセスが終了していることが判明。CLI実行に切り替え、set_time_limit(0) を追加することで解消しました。


② メモリ上限——「件数が多いほど早く止まる」

メモリ問題のサインは「処理件数が増えるほど止まるタイミングが早くなる」ことです。デフォルトの memory_limit は128MBが多く、大量レコードを一括取得するとあっという間に枯渇します。

よくあるアンチパターン:

// ❌ 全件を一度にメモリに展開する
$records = DB::table('orders')->get(); // 10万件→即OOM

修正後:チャンク処理で分割取得

// ✅ chunk()で分割処理
DB::table('orders')->orderBy('id')->chunk(500, function ($records) use ($logger) {
    foreach ($records as $record) {
        processOrder($record);
    }
    // 処理後にガベージコレクションを促す
    gc_collect_cycles();
    $logger->info('500件処理完了', ['memory' => memory_get_usage(true)]);
});

Laravelを使っている場合、cursor() メソッドも有効です。チャンクとの違いは、カーソルはジェネレータを使って1件ずつストリーミングするため、メモリ効率がさらに高くなります。

// ✅ cursor()で1件ずつ処理(大量データに最適)
foreach (DB::table('orders')->cursor() as $record) {
    processOrder($record);
}

③ 例外の握りつぶし——「エラーがないのに処理が飛ばされる」

最も見つけにくい原因がこれです。try-catchで例外をキャッチしているのに、中身が continue や何もしない空ブロックになっているケース。エラーが出ないまま処理がスキップされます。

// ❌ 例外を握りつぶしている典型例
foreach ($records as $record) {
    try {
        processRecord($record);
    } catch (Exception $e) {
        // 何もしない(握りつぶし)
        continue;
    }
}

修正:最低限ログに残す

// ✅ 例外をログに記録しつつ処理を継続
$errorCount = 0;
foreach ($records as $record) {
    try {
        processRecord($record);
    } catch (Exception $e) {
        $errorCount++;
        $logger->error('レコード処理エラー', [
            'id'      => $record->id,
            'message' => $e->getMessage(),
            'trace'   => $e->getTraceAsString(),
        ]);
        // エラー率が高ければ異常終了させる
        if ($errorCount > 100) {
            throw new RuntimeException('エラー件数が上限を超えました');
        }
    }
}

3つの原因を素早く判断するフローチャート

flowchart TD
    A[バッチが途中で止まった] --> B{エラーログに\n記述がある?}
    B -->|ある| C{メッセージの内容}
    C -->|Maximum execution time exceeded| D[タイムアウト\nset_time_limit対応]
    C -->|Allowed memory size exhausted| E[メモリ上限\nchunk/cursor対応]
    C -->|その他のException| F[例外の伝播確認\nログ実装を見直す]
    B -->|ない| G{処理件数と\n停止タイミングは?}
    G -->|常に同じ時間で止まる| D
    G -->|件数が多いほど早く止まる| E
    G -->|特定のレコードで飛ばされる| H[例外握りつぶし\ntry-catchを見直す]

よくある失敗パターンと対処法

失敗① 本番だけで発生する問題を開発環境で再現できない

ローカルはデータ件数が少ないため、メモリもタイムアウトも問題になりません。「本番で試して初めて気づく」というケースが多く、デバッグのサイクルが遅くなります。

対策: ステージング環境に本番相当のデータ量を用意する。または、バッチの冒頭で現在のメモリ上限・タイムアウト設定をログに出力しておき、環境差異を検知しやすくする。

$logger->info('実行環境情報', [
    'memory_limit'       => ini_get('memory_limit'),
    'max_execution_time' => ini_get('max_execution_time'),
    'php_sapi'           => PHP_SAPI, // 'cli' か 'fpm-fcgi' かを確認
]);

失敗② set_time_limit(0) を入れたが効かない

Webサーバー(Apache/Nginxのphp-fpm)の設定が優先されるケースがあります。php.ini の max_execution_time がサーバー設定でオーバーライドされていると、スクリプト内の変更が無効になります。

対策: phpinfo() や ini_get() で実際に適用されている値を確認する。CLI実行への移行、またはphp-fpmのプールごとの設定変更を検討する。

失敗③ chunk()を使ったのにメモリが減らない

Eloquentのリレーションをwith()でイーガーロードしている場合、chunk()を使ってもリレーション分のオブジェクトが蓄積することがあります。また、イベントリスナーやオブザーバーがモデルを保持し続けるケースも。

対策: EloquentではなくDB::table()(クエリビルダ)を使って生の配列で取得する。モデルが必要な場合はwithoutEvents()でオブザーバーを無効化する。


バックエンド開発でお困りですか?

API設計・DB最適化・システム構築など、ご相談ください

無料で相談する

開発・運用でお困りなら

システム開発

設計から運用まで、堅牢なシステムを構築します

200件以上の制作実績 顧客満足度97% 初回相談無料

※ 通常1営業日以内にご返信します

まとめと次のステップ

PHPバッチ処理の「静かな失敗」は、原因を正しく見極めることが解決への最短ルートです。「エラーが出ない=正常」ではなく、「ログを意図的に設計する」ことが信頼性の高いバッチの第一歩です。

弊社では、Laravelを用いたバッチ処理の設計・リファクタリングを多数手がけており、「動いているけど不安」という既存バッチのコードレビューにも対応しています。「自社のバッチが本当に正常に動いているか確かめたい」という方は、お気軽にご相談ください。

この記事をシェア

システム開発のご相談、受付中です

設計・開発・テスト・運用まで、ビジネスに合ったシステムを構築します。 初回相談は無料です。

※ 1営業日以内にご返信いたします

この技術でお困りなら

無料でプロに相談できます

相談する
AIに無料相談