
How to Monitor Laravel Scheduled Tasks: Complete Guide
Learn three approaches to monitoring Laravel scheduled tasks: Spatie's package, manual HTTP pings, and framework-native auto-discovery. Real code examples, best practices, and troubleshooting tips.
Laravel's task scheduler is elegant—define all scheduled tasks in one place, run a single cron entry, and let Laravel handle the rest. But elegance doesn't prevent failures. Your scheduled tasks can fail silently: backup jobs skip, payment processing stalls, or data synchronization stops—and you won't know until something breaks.
This guide covers three practical approaches to monitoring Laravel scheduled tasks, from self-hosted to fully managed solutions, with real code examples and Laravel-specific best practices.
How Laravel's Task Scheduler Works
Before diving into monitoring, let's review how Laravel scheduling works.
The Basics
In Laravel, you define all scheduled tasks in app/Console/Kernel.php:
protected function schedule(Schedule $schedule)
{
// Run backup daily at 2 AM
$schedule->command('backup:run')->dailyAt('02:00');
// Process queued jobs every minute
$schedule->command('queue:work --stop-when-empty')->everyMinute();
// Generate reports every Monday
$schedule->command('reports:generate')->weeklyOn(1, '08:00');
// Clean up old data monthly
$schedule->command('data:cleanup')->monthlyOn(1, '01:00');
}
Then you add one single cron entry to your server:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
Laravel's scheduler runs every minute, checks which tasks are due, and executes them. Clean, simple, powerful.
Why Monitoring is Critical
Laravel's scheduler is reliable, but it can't protect against:
- Command failures - Your
backup:runcommand throws an exception - Infrastructure issues - Database connections fail, disk is full
- Logic errors - Jobs complete with exit code 0 but produce wrong results
- Missed executions - The scheduler cron job stops running
- Long-running tasks - Jobs hang or take much longer than expected
Without monitoring, these failures are silent. Laravel logs errors, but nobody reads logs until customers complain.
Three Approaches to Monitoring Laravel Tasks
Let's explore three monitoring strategies, from self-hosted to fully managed.
Method 1: Spatie's Laravel Schedule Monitor (Self-Hosted)
Spatie's Laravel Schedule Monitor is a popular open-source package that stores monitoring data in your database.
Installation
composer require spatie/laravel-schedule-monitor
php artisan vendor:publish --provider="Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider" --tag="schedule-monitor-config"
php artisan migrate
Configuration
Add the monitorName() method to your scheduled tasks:
protected function schedule(Schedule $schedule)
{
$schedule->command('backup:run')
->dailyAt('02:00')
->monitorName('backup-job');
$schedule->command('reports:generate')
->weeklyOn(1, '08:00')
->monitorName('weekly-reports');
$schedule->command('data:sync')
->hourly()
->monitorName('hourly-sync');
}
Sync Monitors
The package needs to know which tasks to monitor:
php artisan schedule-monitor:sync
This creates database records for each monitored task. Add this to your deployment process.
How It Works
The package automatically:
- Records when tasks start and finish
- Stores execution duration
- Detects tasks that don't run on schedule
- Logs failures with error messages
Viewing Monitor Status
Check monitor status programmatically:
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTask;
// Get all monitored tasks
$tasks = MonitoredScheduledTask::all();
// Check if a specific task failed recently
$backup = MonitoredScheduledTask::findByName('backup-job');
if ($backup->last_finished_at < now()->subDay()) {
// Backup hasn't run in 24 hours - alert!
}
Setting Up Alerts
The package doesn't include built-in alerting. You need to implement it:
// In app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
// Check for failed monitors every 10 minutes
$schedule->call(function () {
$failedTasks = MonitoredScheduledTask::query()
->where('last_finished_at', '<', now()->subHours(24))
->get();
if ($failedTasks->isNotEmpty()) {
// Send alert (email, Slack, etc.)
Notification::route('slack', config('slack.webhook'))
->notify(new TasksFailedNotification($failedTasks));
}
})->everyTenMinutes();
}
Pros and Cons
Pros:
- ✅ Free and open source
- ✅ Data stays in your database
- ✅ No external service dependency
- ✅ Good for teams that want full control
Cons:
- ❌ Requires manual alerting setup
- ❌ No built-in notification channels
- ❌ Adds database tables and queries
- ❌ Need to build dashboard for visibility
- ❌ Doesn't detect if scheduler cron stops running
- ❌ Manual sync required when adding tasks
Best for: Teams that prefer self-hosted solutions and can build custom alerting.
Method 2: Manual HTTP Pings
The second approach uses HTTP pings to external monitoring services. This is the "dead man's switch" pattern.
Basic Implementation
Add pings to your scheduled tasks:
use Illuminate\Support\Facades\Http;
protected function schedule(Schedule $schedule)
{
$schedule->command('backup:run')
->dailyAt('02:00')
->pingBefore('https://monitor.example.com/ping/backup/start')
->thenPing('https://monitor.example.com/ping/backup/complete')
->onFailure(function () {
Http::post('https://monitor.example.com/ping/backup/fail');
});
}
Laravel's built-in pingBefore(), thenPing(), and onFailure() methods handle the HTTP calls automatically.
Using Environment Variables
Keep monitoring URLs in configuration:
// config/monitoring.php
return [
'enabled' => env('SCHEDULE_MONITORING_ENABLED', false),
'base_url' => env('MONITORING_BASE_URL'),
'monitors' => [
'backup' => env('MONITORING_BACKUP_URL'),
'reports' => env('MONITORING_REPORTS_URL'),
],
];
Then use in your schedule:
protected function schedule(Schedule $schedule)
{
$backupTask = $schedule->command('backup:run')->dailyAt('02:00');
if (config('monitoring.enabled')) {
$backupTask
->pingBefore(config('monitoring.monitors.backup') . '/start')
->thenPing(config('monitoring.monitors.backup') . '/complete')
->onFailure(function () {
Http::post(config('monitoring.monitors.backup') . '/fail');
});
}
}
Advanced: Custom Monitoring Class
For more control, create a monitoring helper:
// app/Services/ScheduleMonitor.php
namespace App\Services;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class ScheduleMonitor
{
public static function track(Event $event, string $monitorName): Event
{
if (!config('monitoring.enabled')) {
return $event;
}
$baseUrl = config('monitoring.base_url');
return $event
->pingBefore("{$baseUrl}/ping/{$monitorName}/start")
->thenPing("{$baseUrl}/ping/{$monitorName}/complete")
->onFailure(function () use ($baseUrl, $monitorName) {
try {
Http::timeout(5)->post(
"{$baseUrl}/ping/{$monitorName}/fail",
['timestamp' => now()->toIso8601String()]
);
} catch (\Exception $e) {
Log::error("Failed to report monitor failure: {$e->getMessage()}");
}
});
}
}
Use it in your schedule:
use App\Services\ScheduleMonitor;
protected function schedule(Schedule $schedule)
{
ScheduleMonitor::track(
$schedule->command('backup:run')->dailyAt('02:00'),
'backup'
);
ScheduleMonitor::track(
$schedule->command('reports:generate')->weekly(),
'reports'
);
}
Handling Failures Gracefully
Ensure monitoring failures don't break your tasks:
protected function schedule(Schedule $schedule)
{
$schedule->command('critical:job')
->daily()
->before(function () {
try {
Http::timeout(3)->post('https://monitor.example.com/ping/job/start');
} catch (\Exception $e) {
// Log but don't fail the job
Log::warning("Monitoring ping failed: {$e->getMessage()}");
}
})
->after(function () {
try {
Http::timeout(3)->post('https://monitor.example.com/ping/job/complete');
} catch (\Exception $e) {
Log::warning("Monitoring ping failed: {$e->getMessage()}");
}
});
}
Pros and Cons
Pros:
- ✅ Simple to implement
- ✅ Works with any monitoring service
- ✅ Minimal code changes
- ✅ Built-in Laravel methods
Cons:
- ❌ Manual setup for each task
- ❌ Need to update code when adding tasks
- ❌ Requires remembering to add monitoring to new tasks
- ❌ Can't auto-discover all scheduled tasks
- ❌ Configuration can get verbose
Best for: Teams with a small number of scheduled tasks who want external monitoring.
Method 3: Framework-Native Auto-Discovery
The third approach uses framework-specific integrations that auto-discover all scheduled tasks.
Using CronRadar with Laravel
Install the Laravel package:
composer require cronradar/laravel
Configure your API key:
php artisan vendor:publish --provider="CronRadar\Laravel\ServiceProvider"
Add your API key to .env:
CRONRADAR_API_KEY=your-api-key-here
CRONRADAR_ENABLED=true
Auto-Discovery
Enable monitoring for all tasks with one line:
use CronRadar\Laravel\Facades\CronRadar;
protected function schedule(Schedule $schedule)
{
// Define your tasks as normal
$schedule->command('backup:run')->dailyAt('02:00');
$schedule->command('reports:generate')->weekly();
$schedule->command('data:sync')->hourly();
// Enable monitoring for ALL tasks
CronRadar::monitorAll();
}
That's it. All scheduled tasks are automatically monitored with:
- Lifecycle tracking (start/success/fail)
- Automatic schedule detection
- Grace period handling
- Duration tracking
Selective Monitoring
Monitor specific tasks only:
protected function schedule(Schedule $schedule)
{
// These will be monitored
$schedule->command('backup:run')
->dailyAt('02:00')
->cronRadar('backup');
$schedule->command('payments:process')
->everyFiveMinutes()
->cronRadar('payments');
// This won't be monitored
$schedule->command('cache:warm')
->everyMinute();
}
Custom Metadata
Include additional context:
$schedule->command('data:sync --source=api')
->hourly()
->cronRadar('api-sync', [
'environment' => app()->environment(),
'source' => 'external-api',
'critical' => true
]);
Per-Task Configuration
Override grace periods and timeouts per task:
$schedule->command('long-report')
->daily()
->cronRadar('long-report', [
'grace_period' => 600, // 10 minutes
'timeout' => 3600 // 1 hour
]);
Pros and Cons
Pros:
- ✅ Auto-discovers all scheduled tasks
- ✅ Zero configuration for most use cases
- ✅ No manual sync needed when adding tasks
- ✅ Framework-native API
- ✅ Centralized monitoring dashboard
- ✅ Built-in alerting (Slack, email, PagerDuty, etc.)
- ✅ Historical data and trends
Cons:
- ❌ Requires external service (paid after trial)
- ❌ Another dependency to manage
- ❌ Data sent to external service
Best for: Teams that want comprehensive monitoring without building infrastructure.
Comparison: Which Approach to Choose?
| Feature | Spatie Package | Manual Pings | Auto-Discovery | |---------|---------------|--------------|----------------| | Setup Time | 30 minutes | 5 min/task | 5 minutes | | New Task Added | Manual sync | Update code | Automatic | | Alerting | Build yourself | Service-dependent | Built-in | | Historical Data | Database queries | Service-dependent | Built-in dashboard | | Cost | Free | Varies | Paid (after trial) | | Maintenance | Self-managed | Minimal | None | | Team Collaboration | Build yourself | Service-dependent | Built-in |
Choose Spatie's package if:
- You prefer self-hosted solutions
- You have time to build custom alerting
- Data sovereignty is critical
- You want full control
Choose manual HTTP pings if:
- You have <10 scheduled tasks
- You're already using a monitoring service
- You want external monitoring without packages
Choose auto-discovery if:
- You have many scheduled tasks
- You want zero-maintenance monitoring
- Team collaboration is important
- You value built-in alerting and dashboards
Best Practices for Laravel Schedule Monitoring
Regardless of which approach you choose:
1. Name Your Tasks Descriptively
// Good
$schedule->command('backup:database')
->daily()
->monitorName('database-backup');
// Bad
$schedule->command('backup:database')
->daily()
->monitorName('task1');
2. Set Realistic Grace Periods
Don't alert immediately if a task misses its exact scheduled time:
// Backup scheduled for 2 AM, alert if not complete by 2:15 AM
$schedule->command('backup:run')
->dailyAt('02:00')
->cronRadar('backup', ['grace_period' => 900]); // 15 minutes
3. Monitor Critical Tasks First
Start with business-critical tasks:
- Database backups
- Payment processing
- Data synchronization
- Report generation
Then expand to less critical tasks.
4. Use Environment-Specific Configuration
Don't monitor in local development:
if (app()->environment('production')) {
CronRadar::monitorAll();
}
Or use environment variables:
if (config('monitoring.enabled')) {
// Enable monitoring
}
5. Test Your Monitoring
Regularly verify that monitoring detects failures:
# Create a test task that fails
php artisan make:command TestFailure
# Add to schedule
$schedule->command('test:failure')
->everyMinute()
->cronRadar('test-failure');
# Verify failure alert triggers
6. Handle Long-Running Tasks
For tasks that take minutes or hours:
$schedule->command('generate:big-report')
->daily()
->timeout(3600) // Laravel timeout
->cronRadar('big-report', [
'timeout' => 3600, // Monitoring timeout
'grace_period' => 600 // Extra time before alerting
]);
7. Group Related Tasks
Use consistent naming for related tasks:
$schedule->command('sync:customers')->hourly()->cronRadar('sync-customers');
$schedule->command('sync:orders')->hourly()->cronRadar('sync-orders');
$schedule->command('sync:products')->daily()->cronRadar('sync-products');
This makes it easy to filter and analyze related tasks.
8. Log Task Output
Always log task output for debugging:
$schedule->command('important:task')
->daily()
->appendOutputTo(storage_path('logs/important-task.log'))
->cronRadar('important-task');
Troubleshooting Common Issues
Task Doesn't Run at All
Check the scheduler cron:
crontab -l | grep schedule:run
Should see:
* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1
Verify cron is running:
sudo systemctl status cron # Ubuntu/Debian
sudo systemctl status crond # CentOS/RHEL
Test scheduler manually:
cd /path-to-project
php artisan schedule:run -v
Task Runs But Monitoring Shows "Never Executed"
Check Laravel environment:
The cron may run under a different environment than your terminal. Add environment to cron:
* * * * * cd /path-to-project && php artisan schedule:run --env=production >> /dev/null 2>&1
Verify monitoring is enabled:
php artisan tinker
>>> config('cronradar.enabled')
=> true
Check network connectivity:
curl -I https://cronradar.com
Monitoring Shows Failures But Task Works
Check exit codes:
Ensure your commands return proper exit codes:
// In your command
public function handle()
{
try {
$this->doWork();
return Command::SUCCESS; // Exit 0
} catch (\Exception $e) {
$this->error($e->getMessage());
return Command::FAILURE; // Exit 1
}
}
Review grace periods:
Task may complete but outside grace period:
// Increase grace period
->cronRadar('task-name', ['grace_period' => 1800]) // 30 minutes
Tasks Run Multiple Times
Check for duplicate cron entries:
crontab -l
Should have only one schedule:run entry.
Verify task definition:
// Each task should be defined once
protected function schedule(Schedule $schedule)
{
// Good: defined once
$schedule->command('backup:run')->daily();
// Bad: defined twice (will run twice)
// $schedule->command('backup:run')->daily();
}
Monitoring Not Auto-Discovering Tasks
Ensure monitorAll() is called:
protected function schedule(Schedule $schedule)
{
$schedule->command('task1')->daily();
$schedule->command('task2')->hourly();
// This must come AFTER task definitions
CronRadar::monitorAll();
}
Check for conditional tasks:
// This task won't be discovered if condition is false during sync
if (config('feature.enabled')) {
$schedule->command('feature:task')->daily();
}
Real-World Example: Complete Setup
Here's a production-ready Laravel schedule with monitoring:
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use CronRadar\Laravel\Facades\CronRadar;
class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule)
{
// Critical: Database backup
$schedule->command('backup:database')
->dailyAt('02:00')
->onOneServer()
->appendOutputTo(storage_path('logs/backup.log'));
// Critical: Process pending payments
$schedule->command('payments:process')
->everyFiveMinutes()
->withoutOverlapping(5)
->appendOutputTo(storage_path('logs/payments.log'));
// Important: Sync external data
$schedule->command('data:sync --source=api')
->hourly()
->onOneServer()
->appendOutputTo(storage_path('logs/sync.log'));
// Reports: Weekly summary
$schedule->command('reports:weekly')
->weeklyOn(1, '08:00')
->emailOutputOnFailure('team@example.com');
// Maintenance: Clean old data
$schedule->command('data:cleanup --days=90')
->monthly()
->appendOutputTo(storage_path('logs/cleanup.log'));
// Maintenance: Clear old sessions
$schedule->command('session:gc')
->daily();
// Enable monitoring for all tasks (production only)
if (app()->environment('production')) {
CronRadar::monitorAll();
}
}
}
Conclusion
Monitoring Laravel scheduled tasks doesn't have to be complex. You have three solid options:
- Spatie's package for self-hosted monitoring with full control
- Manual HTTP pings for simple external monitoring
- Auto-discovery for zero-maintenance framework-native monitoring
The right choice depends on your team size, technical requirements, and how much infrastructure you want to manage.
Start by monitoring your most critical tasks—backups, payments, data synchronization. Once you see the value of proactive monitoring (catching failures before customers do), expand coverage to all scheduled tasks.
The worst monitoring strategy is no monitoring at all. Choose an approach and implement it today.
Monitor all Laravel scheduled tasks automatically. CronRadar's Laravel package auto-discovers tasks, tracks lifecycle, and alerts your team. Get started in 5 minutes →


