Laravel bundles every scheduled task behind a single crontab entry. If that one entry is broken, every scheduled task in the app stops — 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.
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:
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:
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.
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.
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:
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.