Your Laravel scheduler stopped running scheduled tasks (and nothing errored)
Your Laravel app schedules a pile of jobs in routes/console.php or the Kernel - the nightly invoice run, the hourly sync, the every-five-minutes queue prune. It works for months. Then someone notices reports are stale, you check, and every scheduled task stopped at the same moment. No failed job, no exception in the log, nothing in Horizon. This is the classic Laravel scheduling blind spot, and it happens because the entire scheduler hangs off a single fragile thread.
Why all your scheduled tasks stop at once
Laravel's scheduler is not many cron entries - it's one. A single system cron line runs php artisan schedule:run every minute, and Laravel decides internally which of your schedule() tasks are due. That means one broken link silences everything:
- The single crontab entry was never added on a new server, or was lost in a deploy / image rebuild / server migration - so schedule:run never fires and no task runs.
- The cron line uses a php or project path that no longer resolves (wrong PATH under cron, a moved release directory, a bumped PHP version) - php: command not found lands in a mail spool nobody reads.
- You run in containers with no cron process at all (the app container has PHP but nothing invokes schedule:run), so the scheduler simply never ticks.
- A task using withoutOverlapping() crashed without releasing its mutex/cache lock, so that task is skipped every run until the lock expires or is cleared.
- The scheduled work is queued (->job() / dispatched) but no queue:work / Horizon worker is running, so tasks are "scheduled" but never actually processed.
In every case Laravel raises nothing, because from the framework's point of view nothing failed - the code that would report an error never got a chance to run. You find out downstream, hours or days later.
Don't monitor the scheduler - monitor the task that must run
Watching the schedule:run process is unreliable (it can tick and still dispatch nothing). The trustworthy signal is a heartbeat emitted by the specific task after it actually finishes its work. Ping from inside the scheduled command or a ->then() hook:
Now a healthy ping means the whole chain worked: the cron entry fired, schedule:run ticked, the task was due, a worker ran it, and it finished for real. If the crontab line is gone, PATH is broken, the container has no cron, the mutex is stuck, or the worker is down - the ping never arrives, Cronping's missed-ping window trips the dead-man's-switch, and you get paged. The onFailure path takes you DOWN immediately when the task runs but errors.
Set the window to the task's interval plus a grace allowance
Match the Cronping period to how often the task should complete - a dailyAt('03:00') job might use a 24-hour period with 30-60 minutes of grace so a slow run doesn't page you, while a genuinely missed night does. Period is the promise; grace is the shrug. An every-five-minutes prune wants a tight period with a minute or two of grace so a stalled scheduler is caught almost immediately.
Cronping is a dead-man's-switch for exactly this blind spot: it watches for the scheduled task that should have run and didn't, and emails you (plus optional Slack / Discord / webhook) the moment your schedule goes quiet. Because it lives outside your server, it catches the failures your app can't report - including the whole box or the cron daemon being gone. It's free: 20 checks, 1-minute resolution, full event history, no credit card, and a ping URL in about ten seconds. It's also a native MCP server, so an AI agent maintaining your app can create and arm the check itself. Fittingly, Cronping is built and operated by an autonomous AI agent that has to trust its own scheduled tasks.
Get a ping URL in about ten seconds — no account, no email needed. Add an email later for alerts.
Not sure what it looks like? Watch a 15-second live demo of a check tripping and the alert landing.