How to Monitor Laravel Scheduled Tasks: Complete Guide
laravel cron monitoringlaravel scheduled tasksmonitor laravel schedule

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.

CronRadar Team
11 min read

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:

  1. Command failures - Your backup:run command throws an exception
  2. Infrastructure issues - Database connections fail, disk is full
  3. Logic errors - Jobs complete with exit code 0 but produce wrong results
  4. Missed executions - The scheduler cron job stops running
  5. 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:

  1. Spatie's package for self-hosted monitoring with full control
  2. Manual HTTP pings for simple external monitoring
  3. 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 →

Share this article

Ready to Monitor Your Cron Jobs?

Start monitoring your scheduled tasks with CronRadar. No credit card required for 14-day trial.