2026年7月5日
2026年7月5日
WordPressのCronジョブがタイムアウトして完了しない場合の設定変更と処理の分割
WordPressのCronジョブ(WP-Cron)が途中で止まって完了しないときは、まず「どの制限時間で切られているか」をログで特定し、次に実行方法をサーバーのcronとWP-CLIに切り替え、それでも長い処理は小分けにして複数回に分けて実行する、という順で直すのが近道です。この記事では、タイムアウトの見分け方、原因の可能性が高い順の確認と対処、wp-config.php の設定変更、処理を分割するコード例までを説明します。
WP-Cronはどう動き、どこで時間切れになるのか
WP-Cronは、OSのcronのように常に動いている仕組みではありません。誰かがサイトにアクセスしたときに、実行予定を過ぎたイベントがあれば、WordPressが自分自身の wp-cron.php へHTTPリクエスト(ループバック)を送り、その中でイベントを処理します。
この仕組みのため、Cronの処理は「普通のWebリクエスト」と同じ制限を受けます。時間切れを起こす場所は主に次の4つです。
- PHPの
max_execution_time(PHPの実行時間の上限) - PHP-FPMの
request_terminate_timeout(FPMがワーカーを強制終了するまでの時間) - nginxやApache、ロードバランサー、CDNなど前段のタイムアウト
- WordPress自身の実行ロック(
WP_CRON_LOCK_TIMEOUT、既定は60秒)
最後のロックは処理を止めるものではありませんが、ロックの有効時間より長い処理が走っていると、次のアクセスで別のCron実行が始まり、同じジョブが重なって動くことがあります。重複実行が起きると、処理がいつまでも終わらないように見えたり、データが二重に処理されたりします。
まず症状とログを確認する
「完了しない」と感じたら、推測で設定を変える前に、何が起きているかを確認します。
イベントの状態を見る
WP-CLIが使える環境なら、予定されているイベントの一覧を確認できます。
wp cron event list --fields=hook,next_run_gmt,next_run_relative,recurrence
wp cron test
next_run_relative が「過去」のまま残り続けるイベントがあれば、実行されていないか、途中で失敗しています。wp cron test はWP-Cronのループバックが起動できるかを確かめるコマンドです。管理画面で確認したい場合は、イベントの一覧表示と手動実行ができるプラグイン「WP Crontrol」が定番です。WooCommerceなどが使うAction Schedulerのジョブは、管理画面の「ツール」配下などにある予定アクション(Scheduled Actions)の画面で、期限切れや失敗の件数を確認できます(画面名や場所はバージョンで変わることがあります)。
ログで「どこで切られたか」を特定する
wp-config.php でデバッグログを有効にし、PHPのエラーログとWebサーバーのログを合わせて見ます。
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
ログに出る文言で、原因の場所がおおよそ分かります。
Maximum execution time of 30 seconds exceeded:PHPのmax_execution_timeで止まっている- PHP-FPMのログに
execution timed outやterminating:FPMのrequest_terminate_timeoutで止まっている - アクセスログで
wp-cron.phpへのリクエストが 504 や 502:前段のプロキシやロードバランサーで切られている Allowed memory size ... exhausted:時間ではなくメモリ不足で止まっている
調査が終わったら WP_DEBUG は本番で無効に戻してください。ログファイルを公開ディレクトリに置いたままにしないことも大切です。
原因と対処(可能性が高い順)
1. Webリクエストの制限時間内に処理が収まっていない
もっとも多いのがこのケースです。共用サーバーでは max_execution_time が30秒前後に設定されていることが多く、大量の投稿を一度に処理するジョブや、外部APIを何度も呼ぶジョブは収まりません。
対処の基本は、WP-Cronをアクセス起点で動かすのをやめ、サーバーのcronからWP-CLIで実行する方法です。PHPのCLI(コマンドライン版)は、既定で max_execution_time が無制限(0)になっていることが多く、Webサーバーやプロキシのタイムアウトも関係しなくなります。
まず wp-config.php の「編集が必要なのはここまで」の行より上に追記します。変更前にファイルのバックアップを取ってください。
// アクセス起点のWP-Cronを止める(サーバーのcronから実行する前提)
define( 'DISABLE_WP_CRON', true );
次に、サーバーのcrontabに実行予定を登録します。パスは環境に合わせて変えてください。
# 5分ごとに、実行予定を過ぎたイベントを処理する
*/5 * * * * cd /var/www/example.com/public_html && /usr/local/bin/wp cron event run --due-now >> /home/user/logs/wp-cron.log 2>&1
DISABLE_WP_CRON を設定したのにサーバー側のcronを登録しないと、予約投稿やバックアップなどすべての定期処理が止まります。2つの設定は同じ作業の中でまとめて反映してください。WP-CLIが使えないサーバーでは、cronから wp-cron.php をcurlなどで呼ぶ方法もありますが、その場合はHTTP経由のためWebサーバー側の制限時間はそのまま残ります。
2. ジョブが一度にすべてを処理しようとしている
CLIに移しても、処理対象が数万件あれば長時間かかり、途中でメモリ不足やDB接続切れが起こり得ます。自作のジョブなら、1回の実行で処理する件数を決め、残りは次の実行に回す作りに変えるのが根本的な対策です。後半でコード例を示します。
プラグインのジョブが長い場合は、そのプラグインに「1回あたりの処理件数」や「バッチサイズ」の設定がないかを確認します。
3. 実行が重複して終わらないように見える
ロックの有効時間(既定60秒)より長いジョブがあると、次のCron実行が並行して始まる場合があります。長いジョブを抱えているなら、wp-config.php でロック時間を延ばすと重複が起きにくくなります。
// WP-Cronの実行ロックの有効時間(秒)
define( 'WP_CRON_LOCK_TIMEOUT', 300 );
ただし、これは重複を減らすための設定で、ジョブ自体を速くするものではありません。自作ジョブには、後述のように独自のロックも入れておくと安全です。
4. ループバックリクエストが失敗している
Basic認証をかけたステージング環境、海外IPを拒否するWAF、サーバー自身のドメインを解決できないDNS設定などがあると、wp-cron.php へのリクエスト自体が届かず、ジョブが始まりません。「サイトヘルス」にループバックリクエストや予約イベントに関する警告が出ていれば、この可能性が高いです。サーバーのcronからWP-CLIで実行する方式にすれば、ループバックに依存しなくなります。
5. メモリ不足で止まっている
ログにメモリ枯渇のエラーが出ている場合は、時間ではなくメモリが原因です。WP_MEMORY_LIMIT を上げる前に、ジョブが不要なデータまで読み込んでいないかを見直します。投稿IDだけが必要なら 'fields' => 'ids' を指定し、件数の集計が不要なら 'no_found_rows' => true を付けるだけで、読み込む量が大きく減ります。
長い処理を分割するコード例
自作の重いジョブを「1回あたりの件数」と「1回あたりの時間」の両方で区切り、続きを次のイベントとして予約する例です。子テーマの functions.php ではなく、テーマ変更の影響を受けないmu-pluginか独自プラグインに置くことをおすすめします。
<?php
// wp-content/mu-plugins/my-batch-job.php
add_action( 'my_batch_job', 'my_run_batch_job' );
function my_run_batch_job() {
// 独自ロック:前回の実行がまだ動いていれば何もしない
if ( get_transient( 'my_batch_job_lock' ) ) {
return;
}
set_transient( 'my_batch_job_lock', 1, 10 * MINUTE_IN_SECONDS );
$started = microtime( true );
$per_run = 50; // 1回で扱う件数
$time_cap = 20; // 1回の処理時間の目安(秒)
$last_id = (int) get_option( 'my_batch_job_last_id', 0 );
global $wpdb;
$ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT ID FROM {$wpdb->posts}
WHERE post_type = %s AND post_status = %s AND ID > %d
ORDER BY ID ASC LIMIT %d",
'post',
'publish',
$last_id,
$per_run
)
);
foreach ( $ids as $id ) {
my_process_one_post( (int) $id ); // 実際の処理(別途定義)
$last_id = (int) $id;
if ( microtime( true ) - $started > $time_cap ) {
break; // 時間の目安を超えたら、残りは次回に回す
}
}
if ( count( $ids ) === $per_run || ( $ids && $last_id !== (int) end( $ids ) ) ) {
// まだ残りがある:位置を保存して1分後に続きを予約
update_option( 'my_batch_job_last_id', $last_id, false );
wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'my_batch_job' );
} else {
// 全件終了:位置をリセット
delete_option( 'my_batch_job_last_id' );
}
delete_transient( 'my_batch_job_lock' );
}
ポイントは3つあります。1つ目は、処理位置を ID で記録していることです。offset で位置を管理すると、処理中に投稿が増減したときに取りこぼしや二重処理が起きますが、「前回の最後のIDより大きいもの」を取る方式なら起きにくくなります。2つ目は、件数だけでなく経過時間でも打ち切っていることです。1件の処理時間が外部APIの応答などで大きくぶれる場合に効きます。3つ目は、独自ロックで同じジョブの並行実行を防いでいることです。
最初の1回は、日次の定期イベントなどから wp_schedule_single_event( time(), 'my_batch_job' ) で予約すれば、あとは自動的に続きが予約されていきます。処理が大規模で、再試行や失敗の記録まで必要な場合は、WooCommerceにも含まれるライブラリのAction Schedulerを使う選択肢もあります。
変更後の確認方法
設定やコードを変えたら、次の順で確認します。
wp cron event listで、過去の予定のまま残っているイベントがなくなったかを見る- 対象のイベントを
wp cron event run フック名で手動実行し、エラーや時間切れが出ないかを確かめる - crontabに書いたログファイルを数回分確認し、定期的に実行されているかを見る
- デバッグログに時間切れやメモリ不足のエラーが新たに出ていないかを見る
本番サーバーで設定を変える前に、可能であればステージング環境で同じ手順を試してください。レンタルサーバーによっては、cronの登録方法やWP-CLIのパス、PHP CLIのバージョンが管理画面ごとに異なります(2026年10月現在)。CLIのPHPバージョンがWebのものと違うと、プラグインが動かないこともあるため、wp --info で確認しておくと安心です。
まとめ
- WP-Cronはアクセス起点のHTTPリクエストで動くため、PHP・PHP-FPM・プロキシの制限時間をそのまま受ける
- まずデバッグログとWebサーバーのログで、どの制限で止まっているかを特定する
- 根本的な対策は
DISABLE_WP_CRONとサーバーのcron+WP-CLIの組み合わせ。両方を同時に設定する - 長いジョブは件数と時間で区切り、続きを次のイベントに予約する。処理位置はIDで管理し、独自ロックで重複を防ぐ
- 重複実行が気になる場合は
WP_CRON_LOCK_TIMEOUTを延ばす。変更前にバックアップを取り、ステージングで試す