How to Monitor Laravel Cron Jobs (With Alerts)
Laravel's task scheduler is elegant. Define your jobs in app/Console/Kernel.php, add one cron entry to call schedule:run, and you're done. Except for one problem: when those scheduled tasks fail, Laravel doesn't tell anyone.
Your nightly database backup? It crashed three weeks ago. The invoice generation that runs every Monday? Broken since someone updated a dependency. You won't know until a customer asks where their invoice is, or worse, until you need that backup.
This guide shows you how to add monitoring to Laravel scheduled tasks so you get alerted the moment something stops working.
This guide covers Laravel 9, 10, and 11. For Laravel 8 and earlier, see notes inline.
Why Laravel's Built-in Logging Isn't Enough
Laravel logs task output to storage/logs/laravel.log by default. That's fine for debugging, but it requires you to actively check the logs. And if the task never runs at all because the server rebooted or someone misconfigured the crontab, there's nothing to log.
You need something that alerts on the absence of success, not just the presence of errors.
The Solution: Heartbeat Monitoring
The pattern is simple: after your task completes successfully, ping an external URL. If that ping doesn't arrive on schedule, you get an alert.
This catches every failure mode. Task crashed mid-execution? No ping, you get alerted. Server went down? No ping. Crontab deleted? No ping. The monitoring service doesn't care why the ping didn't arrive. It just knows something's wrong.
Adding Monitoring to Laravel Scheduled Tasks
Laravel makes this easy with built-in methods. Here are three approaches, depending on your needs.
Method 1: pingOnSuccess (Recommended)
The cleanest approach. Laravel has native support for pinging URLs after successful task completion:
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('backup:run')
->dailyAt('02:00')
->pingOnSuccess('https://api.cronsignal.io/ping/YOUR_CHECK_ID');
}
That's it. If backup:run completes with exit code 0, Laravel pings the URL. If it fails or throws an exception, no ping is sent.
You can also ping on failure if you want redundant alerting:
$schedule->command('backup:run')
->daily()
->pingOnSuccess('https://api.cronsignal.io/ping/YOUR_CHECK_ID')
->pingOnFailure('https://api.cronsignal.io/ping/YOUR_CHECK_ID/fail');
Method 2: Using then() Callbacks
If you need more control, use the then() callback:
// Requires Laravel 7+ (Http facade)
// Add at top of file: use Illuminate\Support\Facades\Http;
$schedule->command('reports:generate')
->hourly()
->then(function () {
Http::timeout(5)->get('https://api.cronsignal.io/ping/YOUR_CHECK_ID');
});
This runs your custom code after the task succeeds. Useful if you need to include additional data or conditional logic.
Method 3: Closure-Based Tasks
For tasks defined as closures rather than commands:
$schedule->call(function () {
// Your task logic here
SyncService::run();
})->daily()->then(function () {
file_get_contents('https://api.cronsignal.io/ping/YOUR_CHECK_ID');
});
The then() callback only fires if the closure completes without throwing an exception.
Monitoring the Scheduler Itself
Here's something most tutorials miss: you should also monitor that schedule:run is actually being called.
Your crontab has this line:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
If that cron entry gets deleted or the server's cron daemon stops, none of your scheduled tasks run. Add a simple heartbeat task:
$schedule->call(function () {
// Empty task, just proves the scheduler is alive
})->everyMinute()
->pingOnSuccess('https://api.cronsignal.io/ping/SCHEDULER_HEARTBEAT');
Set up CronSignal to expect this ping every minute. If it stops arriving, your entire scheduler is down.
Handling Long-Running Tasks
For tasks that take significant time, you might want to know if they start but never finish:
$schedule->command('etl:run')
->daily()
->before(function () {
Http::get('https://api.cronsignal.io/ping/YOUR_CHECK_ID/start');
})
->pingOnSuccess('https://api.cronsignal.io/ping/YOUR_CHECK_ID');
This pings when the task starts and again when it completes. If you get a start ping but no completion ping, the task is hanging or crashed mid-execution.
Queue Workers vs Scheduled Tasks
Don't confuse these. Laravel's scheduler runs discrete tasks on a schedule. Queue workers process jobs continuously from a queue.
For queue workers, you want different monitoring. Supervisor keeps them running, and you should monitor the queue length and processing time. That's a different problem than scheduled task monitoring.
For scheduled tasks that dispatch jobs to the queue, monitor the scheduled task itself, not the queued job:
$schedule->job(new ProcessReportsJob)
->daily()
->pingOnSuccess('https://api.cronsignal.io/ping/YOUR_CHECK_ID');
This confirms the job was dispatched. You'll need separate monitoring for whether the queue actually processed it.
Testing Your Setup
Before relying on monitoring, verify it works.
First, run a task manually and confirm the ping arrives:
# Laravel 9+ only
php artisan schedule:test
# Select your task from the list
# For Laravel 8 and earlier, run the command directly:
php artisan backup:run
Check CronSignal to see the ping registered.
Then test failure alerting. Comment out the task logic temporarily and run it again. The ping shouldn't arrive, and you should get an alert within your configured grace period.
Getting Started
CronSignal handles the monitoring side of this for $9/month for unlimited checks. Create a check, grab your ping URL, add it to your scheduled task, and you're done. Start with 3 checks free.
The setup takes two minutes. The peace of mind when your 2 AM backup actually fails and you find out at 2:15 AM instead of next month? That's worth it.
For more on the general pattern of heartbeat monitoring and why it beats other approaches, see our guide on how to monitor cron jobs.