Laravel Schedule Monitor: Know When Your Scheduled Tasks Stop Running

Laravel Schedule Monitor: Know When Your Scheduled Tasks Stop Running

Picture this. You set up a daily database backup task six months ago. It ran perfectly during testing, you pushed it to production, and you completely forgot about it. Fast forward to today — your client calls saying their data from three weeks ago is gone and they need it restored. You open the server, check the backups folder… and it’s empty. The task had been silently failing for weeks because of a missing environment variable, and nobody — not you, not Laravel, not your server — sent a single alert. If you’ve been using a Laravel schedule monitor, this story ends very differently.

The Problem With Unmonitored Laravel Scheduled Tasks

Laravel’s task scheduler is genuinely great. You define tasks in Kernel.php (or routes/console.php in Laravel 11+), set up a single cron entry on your server, and boom — your tasks run like clockwork.

Except when they don’t.

The problem is Laravel gives you zero visibility into what actually happens after a task fires. Did it finish? Did it throw an exception? Did it even start? You won’t know unless you’re actively checking logs — and let’s be honest, nobody does that daily.

Here’s what can silently go wrong in production:

  • A daily report stops generating because a third-party API key expired
  • A database backup fails because disk space ran out
  • An email digest stops sending because a mail driver config changed after a deploy
  • A queue cleanup task crashes due to a model change nobody accounted for

None of these send you a notification. Your users will find out before you do. That’s the real problem — not that tasks fail, but that you’re the last one to know.

What Is spatie/laravel-schedule-monitor?

spatie/laravel-schedule-monitor is an open-source package from the team at Spatie that brings visibility to your Laravel schedule. It logs every task start, finish, failure, and skip to your database — and optionally integrates with Oh Dear to send real-time alerts when a task fails or doesn’t run on time.

The package gives you:

  • A database log of every scheduled task run, including execution time and memory usage
  • A simple CLI command to list all monitored tasks and their last run status
  • A grace time system that flags tasks as “late” if they don’t finish within an expected window
  • Optional Oh Dear integration for external cron monitoring and notifications via Slack, email, SMS, or webhooks

It’s one of those packages that you install once and immediately wonder how you ever shipped to production without it.

Installing and Setting Up the Package (Step by Step)

Step 1 — Install via Composer

PHP
composer require spatie/laravel-schedule-monitor

That’s it for the install. No manual service provider registration needed for Laravel 10+, auto-discovery handles it.

If you’re on Laravel 8, use composer require spatie/laravel-schedule-monitor:^2 instead.

Step 2 — Publish and Run Migrations

The package needs a couple of database tables to store task logs.

PHP
php artisan vendor:publish --provider="Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider" --tag="schedule-monitor-migrations"

php artisan migrate

This creates two tables: monitored_scheduled_tasks and monitored_scheduled_task_log_items. The first tracks which tasks exist, the second logs every run.

Step 3 — Publish the Config File

PHP
php artisan vendor:publish --provider="Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider" --tag="schedule-monitor-config"

This drops a config/schedule-monitor.php file into your project. You don’t need to change anything right now to get basic monitoring working — the defaults are sensible.

Step 4 — Sync Your Schedule

This is the key command. It reads your schedule and registers all tasks into the database.

PHP
php artisan schedule-monitor:sync

You’ll see output listing every task that was found and synced. Run this command every time you deploy — add it to your deploy script right next to php artisan migrate.

Step 5 — View Your Monitored Tasks

PHP
php artisan schedule-monitor:list

This gives you a beautiful table showing every scheduled task, when it last ran, whether it succeeded or failed, and how long it took. If something ran late or failed, it shows up in red. Clean. Useful. Satisfying.

Monitoring Tasks and Getting Failure Alerts

Understanding “Failed” vs “Missed”

These are two different things and worth knowing upfront.

  • Failed means the task ran but threw an exception or exited with a non-zero status
  • Missed (or “late”) means the task didn’t finish within the expected time window — the scheduled run time plus the grace time

By default, every task gets a 5-minute grace time. You can customize this per task using graceTimeInMinutes().

PHP
// app/Console/Kernel.php

protected function schedule(Schedule $schedule)
{
    $schedule->command('reports:generate')
        ->daily()
        ->graceTimeInMinutes(10); // give it 10 minutes to finish
}

If the task doesn’t complete within 10 minutes of its scheduled time, the package marks it as late.

Setting Up Oh Dear for External Alerts

The package’s built-in DB logging is great, but you still have to remember to check schedule-monitor:list. For real production monitoring, connect it to Oh Dear — an uptime and cron monitoring service that actively pings you when something goes wrong.

Add these to your .env:

PHP
OH_DEAR_API_TOKEN=your-api-token-here
OH_DEAR_MONITOR_ID=your-monitor-id-here

Then verify the connection:

PHP
php artisan schedule-monitor:verify

After that, sync again:

PHP
php artisan schedule-monitor:sync

Oh Dear will now receive a ping every time a task starts and finishes. If a task doesn’t send its “finished” ping within the grace time, Oh Dear fires a notification — Slack, email, SMS, webhooks, your call.

Pro tip: Create a dedicated queue for the Oh Dear ping jobs. Add OH_DEAR_QUEUE=ohdear to your .env and make sure your queue worker processes it. This keeps ping delivery snappy and avoids false positives.

Skipping Monitoring for Specific Tasks

Not every task needs to be watched. Some tasks are trivial and you genuinely don’t care if they skip a run. Use doNotMonitor() for those.

PHP
$schedule->command('telescope:prune')->daily()->doNotMonitor();

You can also store task output directly in the log for debugging:

PHP
$schedule->command('reports:generate')->daily()->storeOutputInDb();

That output lands in the meta column of the log table. Useful when you want to know why something failed without SSHing into the server.

Real-World Example — Daily Database Backup Task

Let’s build a full example. Say you have a daily database backup command, and you want to monitor it properly.

Create the Artisan Command

PHP
php artisan make:command BackupDatabase
PHP
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class BackupDatabase extends Command
{
    protected $signature = 'backup:database';
    protected $description = 'Create a daily database backup';

    public function handle()
    {
        $filename = 'backup-' . now()->format('Y-m-d-H-i-s') . '.sql';
        $path = storage_path('app/backups/' . $filename);

        // Create backups directory if it doesn't exist
        if (!file_exists(storage_path('app/backups'))) {
            mkdir(storage_path('app/backups'), 0755, true);
        }

        $dbName = config('database.connections.mysql.database');
        $dbUser = config('database.connections.mysql.username');
        $dbPass = config('database.connections.mysql.password');
        $dbHost = config('database.connections.mysql.host');

        $command = "mysqldump --user={$dbUser} --password={$dbPass} --host={$dbHost} {$dbName} > {$path}";

        exec($command, $output, $returnCode);

        if ($returnCode !== 0) {
            $this->error('Database backup failed.');
            return self::FAILURE;
        }

        $this->info("Backup created: {$filename}");
        return self::SUCCESS;
    }
}

Register It in the Scheduler

Laravel 10 and below — app/Console/Kernel.php:

PHP
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem;

protected function schedule(Schedule $schedule)
{
    // Monitor your backup task with a 15-minute grace window
    $schedule->command('backup:database')
        ->dailyAt('02:00')
        ->graceTimeInMinutes(15)
        ->storeOutputInDb()
        ->monitorName('daily-database-backup');

    // Prune old log items so your DB doesn't bloat
    $schedule->command('model:prune', [
        '--model' => MonitoredScheduledTaskLogItem::class
    ])->daily();
}

Laravel 11+ — routes/console.php:

PHP
use Illuminate\Support\Facades\Schedule;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem;

Schedule::command('backup:database')
    ->dailyAt('02:00')
    ->graceTimeInMinutes(15)
    ->storeOutputInDb()
    ->monitorName('daily-database-backup');

Schedule::command('model:prune', [
    '--model' => MonitoredScheduledTaskLogItem::class
])->daily();

What Happens When It Fails?

If the backup command returns FAILURE, the package logs it as a failed run in monitored_scheduled_task_log_items. Running schedule-monitor:list will show it with a red background.

If you’ve connected Oh Dear, you’ll get a notification immediately — before your client even notices something’s wrong.

If the task simply doesn’t run at all (say your cron entry broke after a server migration), Oh Dear pings you when the grace window expires because it never received the “finished” signal.

That’s the safety net most production apps are missing.

Why Every Production Laravel App Needs This

Here’s an honest take: skipping this package is like writing code with no error handling and just hoping the happy path always holds. It usually does — right up until the moment it doesn’t.

Scheduled tasks fail for completely mundane reasons. Disk fills up. An API changes. A deploy removes an env variable. A dependency breaks. These aren’t edge cases — they’re Tuesday.

The difference between a team that looks professional and one that looks reactive almost always comes down to whether they found out about the problem or their client did.

schedule-monitor:list takes two seconds to run. Setting up the package takes under ten minutes. The Oh Dear integration adds maybe another five. For that investment, you get full visibility into every scheduled task in your app, forever.

If you’re running any of the following in production, this package is not optional:

  • Daily or weekly report generation
  • Database backups
  • External API syncs
  • Email or notification dispatching
  • Queue cleanup or data pruning tasks

If it runs on a schedule and your business depends on it — monitor it.

The spatie/laravel-schedule-monitor package is one of those rare tools that plugs a real gap rather than adding complexity. It’s lightweight, well-maintained, and does exactly what it says.

Go install it today: github.com/spatie/laravel-schedule-monitor

Run the sync, check the list, and if you’re serious about production stability, wire it up to Oh Dear. You’ll sleep better knowing your 2am backup task is actually running — and you’ll hear about it immediately if it ever stops.

Your future self will thank you.


Found this useful? Share it with a fellow Laravel dev who’s probably running unmonitored scheduled tasks right now — there are a lot of them.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *