CronCanary DocsPricingOpen app
← Integration guides

Monitor Laravel's task scheduler

Laravel bundles every scheduled task behind a single crontab entry. If that one entry is broken, every scheduled task in the app stops — silently.

Why Laravel's scheduler fails silently

Laravel deliberately doesn't use one crontab line per scheduled task. Instead you add exactly one system cron entry — * * * * * php artisan schedule:run — and every Schedule::command()/Schedule::call() registered in your app is evaluated in-process each minute against its own frequency. That design is convenient, but it means a single point of failure: if the server's cron daemon isn't running, if that one crontab line was never deployed to a new box, if php resolves to the wrong version after a server migration, or if the app's service container fails to boot (a bad .env, a broken cached config), schedule:run either never fires or dies before it evaluates a single task — and every scheduled job in the application goes quiet at once, with nothing in storage/logs/laravel.log because the app never got far enough to log anything. Laravel's own overlap protection (withoutOverlapping()) and output emailing (emailOutputTo()) only help once a task actually starts running — they can't tell you the scheduler itself stopped being invoked.

Use the built-in ping hooks

Laravel ships ping methods on the scheduled-event builder specifically for this: pingBefore()/thenPing() fire regardless of outcome, and pingOnSuccess()/pingOnFailure() fire only for a zero or non-zero exit code respectively. They require Guzzle, which Laravel installs by default:

// app/Console/Kernel.php (or bootstrap/app.php on Laravel 11+ using Schedule::) $url = "$URL"; // e.g. https://croncanary-ping.sleeezydesigns.workers.dev/<your-uuid> Schedule::command('reports:nightly') ->dailyAt('04:00') ->pingBefore("$url/start") ->pingOnSuccess($url) ->pingOnFailure("$url/fail");

Watch the scheduler itself, not just one task

A per-task ping only proves that particular command ran. To catch the crontab entry disappearing entirely — the failure mode described above — add a trivial always-on task that pings on every scheduler tick, independent of any real job:

Schedule::call(function () use ($url) { Http::get($url); // heartbeat: proves schedule:run itself is still being invoked })->everyMinute();

Give that check a Simple schedule of 60 seconds with a short grace period (e.g. 3 minutes). If it goes down, the system cron entry, PHP itself, or the app's bootstrap is broken — before you find out from a customer that reports never went out.

Match the schedule and timezone

Set the check's Cron expression to mirror the frequency helper you used (dailyAt('04:00')0 4 * * *) and set the check's timezone to whatever $schedule->timezone() or config('app.timezone') resolves to — Laravel's scheduler evaluates frequencies in that timezone, and a mismatch here is the single most common cause of a false "missed run" alert.


Related guides


Add a live status badge to your README

Every check has a public SVG badge that shows its live status (updates within ~1 minute). Paste this into any README — it doubles as a heartbeat anyone on the team can see:

[![CronCanary](https://croncanary.fluxath.app/badge/<your-check-id>.svg)](https://croncanary.fluxath.app)

Copy the exact markdown from your check's detail page. Add ?label=your-text to customize the left label.


Ready to wire this up? Create a free check — 20 checks, all alert channels, no card.