2026年5月24日

2026年5月24日

Laravelのスケジューラー(Task Scheduling)設定:cron 1行での定期実行と重複・失敗への備え

Laravel のスケジューラーを使うと、定期的に動かしたい処理をすべて PHP のコードで定義し、サーバーの cron には「毎分 schedule:run を呼ぶ」1行だけを登録すれば済みます。処理ごとに cron を書き足す必要がなく、実行間隔や重複防止、失敗時の通知まで Git で管理できます。この記事では、スケジュールの定義場所と書き方、cron の登録、重複実行と複数サーバーへの備え、ログと失敗の検知、動作確認のコマンド、つまずきやすい点までを説明します。

スケジューラーの仕組み

スケジューラーは、それ自体が常駐して時間を待つものではありません。cron が毎分 php artisan schedule:run を実行し、そのたびに Laravel が「今この分に実行すべきタスクはどれか」を判断して実行します。

この仕組みから、次のことが分かります。

  • cron の登録を忘れると、コードにスケジュールを書いても何も実行されません。
  • 判断は毎分行われるため、指定時刻のずれは基本的に1分以内です。
  • 同じサーバーで schedule:run が重複して呼ばれると、タスクも重複して実行されます。

スケジュールを定義する場所

Laravel 11 以降では、routes/console.php に Schedule ファサードを使って書きます。それより前の版では、app/Console/Kernel.php の schedule() メソッドに $schedule->command(...) の形で書きます(2026年10月現在)。書き方の違いは定義の場所だけで、使えるメソッドはほぼ共通です。以下は Laravel 11 以降の書き方で説明します。

<?php
// routes/console.php

use App\Jobs\SyncInventory;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;

// Artisan コマンドを毎日 3:00 に実行
Schedule::command('reports:daily')
    ->dailyAt('03:00')
    ->timezone('Asia/Tokyo');

// キューのジョブを 15 分ごとに投入
Schedule::job(new SyncInventory)->everyFifteenMinutes();

// クロージャーで簡単な処理を毎週月曜 4:00 に実行
Schedule::call(function () {
    DB::table('login_attempts')
        ->where('created_at', '<', now()->subDays(30))
        ->delete();
})->weeklyOn(1, '4:00');

// シェルコマンドも実行できる
Schedule::exec('find /var/www/app/storage/exports -mtime +7 -delete')->daily();

タスクの種類ごとの使い分けは次のとおりです。

  • **command()**: Artisan コマンドを実行します。処理の本体をコマンドとして作っておくと、手動でも同じ処理を実行できるので、テストや障害時の再実行が楽になります。多くの場合はこれを選びます。
  • **job()**: キューにジョブを投入します。時間のかかる処理をワーカーに任せたいときに使います。キューのワーカーが動いていないと、投入されたまま実行されません。
  • **call()**: クロージャーを直接実行します。数行で済む処理向けです。長くなるならコマンドに切り出します。
  • **exec()**: OS のコマンドを実行します。削除のような操作は、対象のパスを間違えると影響が大きいので、ステージングで確認してから使います。

実行間隔の指定

よく使うメソッドを挙げます。

  • everyMinute()、everyFiveMinutes()、everyFifteenMinutes()、hourly()
  • daily()(0:00)、dailyAt('03:00')、twiceDaily(1, 13)
  • weekly()、weeklyOn(1, '4:00')(曜日は 0 が日曜)、monthly()
  • cron('0 */6 * * *'): cron と同じ書式で指定
  • weekdays()、between('9:00', '18:00') などの条件の追加

時刻の基準はアプリケーションのタイムゾーンです。config/app.php の timezone が UTC のままだと、dailyAt('03:00') は日本時間の正午に実行されます。タスクごとに timezone('Asia/Tokyo') を付けるか、アプリ全体の設定を見直してください。夏時間のあるタイムゾーンでは、切り替えの日に実行されない、または2回実行されることがある点も公式ドキュメントで注意されています。

新しい版では everyThirtySeconds() のように1分未満の間隔も指定できますが、その間は schedule:run が終了せずに動き続ける仕組みになります。本当に1分未満が必要かを先に検討し、多くの場合はキューのワーカーで処理するほうが扱いやすくなります。

cron に1行を登録する

本番サーバーでは、アプリケーションを動かしているユーザーの crontab に次の1行を登録します。root で登録すると、作られるログやキャッシュのファイルが root の所有になり、Web 側から書き込めなくなる原因になります。

sudo -u www-data crontab -e
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

php が見つからない、または Web とは別の版の PHP が使われる場合は、/usr/bin/php8.3 のようにフルパスで指定します。cron は環境変数が最小限の状態で動くため、ログインしたシェルで動くコマンドが cron では動かない、というのはよくある原因です。

開発環境では cron を設定せず、次のコマンドをターミナルで動かしておけば、毎分のスケジュールが実行されます。

php artisan schedule:work

Docker で動かす場合は、アプリのコンテナとは別にスケジューラー用のコンテナを用意し、そこで schedule:work を動かす構成がよく使われます。

重複実行と複数サーバーへの備え

前の実行が終わらないうちに次が始まる

5分ごとのタスクが、データ量が増えて6分かかるようになると、前の実行が終わる前に次が始まります。これを防ぐのが withoutOverlapping() です。

Schedule::command('feeds:import')
    ->everyFiveMinutes()
    ->withoutOverlapping(10);   // ロックの有効期限(分)

実行中はロックが取られ、終わるまで次の実行は飛ばされます。ロックの有効期限を指定しないと既定では長時間(ドキュメント上は24時間)残るため、プロセスが強制終了されてロックが解放されなかった場合、その間タスクが止まります。処理時間の目安より少し長い値を指定しておくと、復旧が早くなります。

複数のサーバーで同じタスクが動く

Web サーバーを複数台にして、すべてで cron を登録すると、同じタスクが台数分実行されます。onOneServer() を付けると、最初にロックを取ったサーバーだけが実行します。

Schedule::command('reports:daily')
    ->dailyAt('03:00')
    ->onOneServer();

このロックはキャッシュを通じてサーバー間で共有されるため、既定のキャッシュに Redis やデータベースなど、全サーバーから同じものが見える保存先を使う必要があります。サーバーごとのファイルキャッシュでは共有されず、重複を防げません。

時間のかかるタスクを並行させる

同じ時刻に予定されたタスクは、既定では順番に実行されます。1つが長引くと後ろのタスクの開始が遅れるため、時間のかかるものには runInBackground() を付けます。これは command() と exec() で使えます。

ログと失敗の検知

スケジュールの出力は、そのままでは cron の行の >> /dev/null で捨てられます。タスクごとに出力の保存先を決めておくと、後から調べられます。

Schedule::command('reports:daily')
    ->dailyAt('03:00')
    ->appendOutputTo(storage_path('logs/reports-daily.log'))
    ->onFailure(function () {
        logger()->error('reports:daily が失敗しました');
    });

appendOutputTo() はファイルに追記し、sendOutputTo() は毎回上書きします。onSuccess() と onFailure() で、成功時と失敗時の処理を書けます。失敗の判定はコマンドの終了コードで行われるので、自作のコマンドでは失敗時に Command::FAILURE を返すようにしておきます。

ただし、onFailure() は「タスクが実行されて失敗した」ときにしか動きません。cron の設定が消えた、サーバーが止まっていた、といった「そもそも実行されなかった」場合は検知できません。重要なタスクは、成功時に外部の死活監視サービスの URL を呼ぶ thenPing() などを使い、一定時間連絡がなければ通知される仕組みと組み合わせると安心です。

動作確認のコマンド

php artisan schedule:list    # 登録されたタスクと次回の実行予定を一覧表示
php artisan schedule:test    # タスクを選んで今すぐ実行する

schedule:list で、タイムゾーンを含めて意図した時刻になっているかを確認します。schedule:test は時刻に関係なく実行するので、データを変更するタスクは開発環境かステージングで試してください。

つまずきやすい点

  • 何も実行されない: cron が登録されていない、別のユーザーの crontab に登録した、cd のパスが違う、のいずれかが多い原因です。cron の行の出力先を一時的にファイルにして、エラーが出ていないか確認します。
  • 時刻がずれる: アプリのタイムゾーンが UTC のままになっています。schedule:list で次回の実行予定を確認します。
  • ジョブが実行されない: job() で投入したジョブは、キューのワーカーが処理します。queue:work が動いているか確認します。
  • メンテナンス中に止まる: php artisan down の間、タスクは既定で実行されません。止めたくないタスクには evenInMaintenanceMode() を付けます。
  • デプロイ直後に古いコードで動く: 常駐している schedule:work やキューのワーカーは、デプロイ後に再起動しないと古いコードのまま動くことがあります。デプロイ手順に再起動を含めます。

まとめ

  • スケジューラーは、cron から毎分呼ばれる schedule:run が実行するタスクを判断する仕組みで、cron の登録が前提です。
  • Laravel 11 以降は routes/console.php、それより前は app/Console/Kernel.php に定義し、処理の本体は Artisan コマンドにすると再実行しやすくなります。
  • タイムゾーンを確認し、長引くタスクには withoutOverlapping()、複数サーバーでは共有キャッシュと onOneServer() を使います。
  • 出力の保存先と onFailure() を設定し、実行されなかったことを検知する外部の監視も組み合わせます。
  • schedule:list と schedule:test で予定と動作を確認します。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲