Monitoring the Laravel task scheduler
Laravel’s scheduler keeps all your periodic tasks in code, behind a single crontab line. That single line is also the weak spot: if it’s missing on a new server, nothing scheduled runs and Laravel has no way to tell you. The good news is that the scheduler can ping a monitoring URL by itself, so watching it takes a few lines.
Ping from each task
Since Laravel 11, schedules live in routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('backup:run')
->dailyAt('03:17')
->withoutOverlapping()
->pingBefore('https://ping.heartcron.com/<uuid>/start')
->pingOnSuccess('https://ping.heartcron.com/<uuid>')
->pingOnFailure('https://ping.heartcron.com/<uuid>/fail');
In Laravel 10 and earlier the same chain goes in the schedule() method of app/Console/Kernel.php, starting with $schedule->command(...).
pingBeforereports the start, so a task that hangs is noticed.pingOnSuccessandpingOnFailurereport the result. Laravel treats a non-zero exit code of the Artisan or shell command as a failure, so these work best withcommand()andexec()tasks.- The ping methods use Guzzle, which new Laravel apps already include.
Keep URLs out of the code and ping only from production:
// config/services.php 'heartcron' => [ 'backup' => env('HEARTCRON_BACKUP_URL'), ], // routes/console.php $url = config('services.heartcron.backup'); Schedule::command('backup:run') ->dailyAt('03:17') ->environments(['production']) ->pingBefore("$url/start") ->pingOnSuccess($url) ->pingOnFailure("$url/fail");
Watch the scheduler itself
Every scheduled task depends on this crontab entry:
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1
If it goes missing, the per-task checks will eventually fire, but a weekly task would stay silent for a week. Add a heartbeat that runs every five minutes and does nothing but ping:
Schedule::call(fn () => null)
->everyFiveMinutes()
->name('heartbeat')
->thenPing(config('services.heartcron.scheduler'));
Give it a check with a 5-minute period and a 5-minute grace period. When the crontab line disappears or the server stops, you know within ten minutes.
Several servers
If the app runs on more than one server, add ->onOneServer() to tasks that must run once. It needs a cache driver shared by all servers, such as Redis or a database. The ping then comes from whichever server ran the task, and the check still sees exactly one run.
In containers
Instead of cron, run php artisan schedule:work as the main process of a separate container. It runs the scheduler every minute in the foreground. The heartbeat task above tells you when that container stops.