Skip to main content
All posts
· 5 min read

Laravel Scheduler Events: Build Custom Monitoring With Before and After Hooks

Laravel's scheduler dispatches events on task start, finish, and failure. Learn how to hook into ScheduledTaskFinished, listen for failures, and wire real-time alerts — no external service required.

Most Laravel teams monitor cron jobs the same way: check if the process runs at all, then hope nothing breaks. But Laravel’s scheduler fires a rich set of events — ScheduledTaskStarted, ScheduledTaskFinished, and ScheduledTaskFailed — that let you build custom monitoring into your application itself.

This isn’t about replacing an external monitoring service. It’s about catching failures before your heartbeat check misses its window.

What Laravel Scheduler Events Actually Cover

When the scheduler runs a command, it dispatches events at each lifecycle stage:

EventFired whenWhat you get
ScheduledTaskStartingBefore a task executesThe task expression, command string
ScheduledTaskStartedAfter a task starts executionTask + process ID
ScheduledTaskFinishedAfter a task completes (any exit code)Task + output + exit code
ScheduledTaskFailedWhen a task throws an uncaught exceptionTask + exception instance

The critical one most teams miss: ScheduledTaskFailed. If your artisan command throws halfway through, the scheduler catches it and dispatches this event — but nobody is listening.

Wiring a Scheduler Event Listener

// app/Providers/EventServiceProvider.php

use Illuminate\Console\Events\ScheduledTaskFailed;
use Illuminate\Console\Events\ScheduledTaskFinished;
use App\Listeners\ReportSchedulerFailure;

protected $listen = [
    ScheduledTaskFailed::class => [
        ReportSchedulerFailure::class,
    ],
    ScheduledTaskFinished::class => [
        LogSchedulerOutput::class,
    ],
];

The listener gets access to the exception and the full task context:

// app/Listeners/ReportSchedulerFailure.php

<?php

namespace App\Listeners;

use Illuminate\Console\Events\ScheduledTaskFailed;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;

class ReportSchedulerFailure
{
    public function handle(ScheduledTaskFailed $event): void
    {
        Log::warning('Scheduled task failed', [
            'command' => $event->task->command,
            'expression' => $event->task->expression,
            'exception' => $event->exception->getMessage(),
        ]);

        // Send to Crontinel or wherever you track production health
        Http::post(config('crontinel.heartbeat_url'), [
            'status' => 'failed',
            'task' => get_class($event->task),
            'error' => $event->exception->getMessage(),
            'time' => now()->toIso8601String(),
        ]);
    }
}

Why This Matters When Your Scheduler Seems Fine

A heartbeat check tells you “the cron daemon ran at 02:00.” It does not tell you:

  • Did the command throw an exception mid-execution?
  • Did it exit with code 1 because a service was unreachable?
  • Did it silently skip the real work because of a logic gate?

These are execution failures, not scheduler failures. And they’re the most common failure mode in production Laravel apps.

// Example: A seemingly healthy cron that fails silently
$schedule->command('emails:send-digest')
    ->dailyAt('06:00')
    ->withoutOverlapping();  // If the previous run didn't clean up, this silently skips

The scheduler runs. The process starts. But withoutOverlapping() prevents execution, no output is generated, no failure is logged, and your heartbeat arrives exactly on time — with zero actual work done.

Building a Scheduler Event Dashboard

You can log scheduler events to your database for a real-time dashboard:

// database/migrations/create_scheduler_events_table.php

Schema::create('scheduler_events', function (Blueprint $table) {
    $table->id();
    $table->string('task');
    $table->string('expression')->nullable();
    $table->string('status'); // started, finished, failed
    $table->integer('exit_code')->nullable();
    $table->text('output')->nullable();
    $table->text('error_message')->nullable();
    $table->timestamp('ran_at');
    $table->timestamps();
});

Then listen to all scheduler events and write them to this table. Now you can query: “Which tasks failed most in the last 7 days?” or “Did the backup task run within its expected window?”

The Complete Monitoring Picture

Scheduler events give you inside-out visibility. Pair them with outside-in heartbeat monitoring for full coverage:

  • Inside-out (this approach): Catches exceptions, exit codes, and execution failures that a heartbeat can’t see.
  • Outside-in (Crontinel or similar): Verifies the scheduler process actually runs at the right time and alerts if the entire cron cycle is missing.

A task that fails internally but still runs on schedule is the hardest scenario to debug. Scheduler events catch that gap.

Wiring External Alerts

Use the ScheduledTaskFinished event to forward output and exit codes to an external monitor:

use Illuminate\Console\Events\ScheduledTaskFinished;

public function handle(ScheduledTaskFinished $event): void
{
    if ($event->task->exitCode !== 0) {
        // Non-zero exit: forward to Crontinel for alerting
        Http::post(config('crontinel.heartbeat_url'), [
            'status' => 'degraded',
            'exit_code' => $event->task->exitCode,
            'output' => substr($event->task->output ?? '', 0, 2000),
        ]);
    }
}

Now you know about failures within seconds, not hours.

Enabling Scheduler Events (Laravel 9+)

Scheduler events are enabled by default since Laravel 9. If you’re on an older version, make sure schedule:run isn’t running with --no-interaction in a way that suppresses event dispatch. The framework handles this automatically in modern versions — just register your listeners.

See also

blog
PagerDuty + Laravel: Auto-Creating Incidents from Queue and Cron Alerts

Wire Laravel queue depth alerts and cron failures directly into PagerDuty using Events API v2. Dedup keys keep fire and resolve events linked to one incident so oncall doesn't get spammed.

use cases
How to Monitor php artisan queue:monitor in Production

Monitor Laravel queue:monitor in production so you catch backed-up queues, QueueBusy events, and scheduler failures before the backlog turns into an incident.

blog
Cron Monitoring Alert Fatigue: How to Reduce Notification Noise

Too many cron monitoring alerts desensitize your team to real incidents. Here's how to classify severity, set smart grace periods, and build alerting rules that cut noise without missing real failures.

blog
Node.js Cron Monitoring: Detect Scheduled Task Failures Before Users Do

Complete guide to monitoring Node.js cron jobs, scheduled tasks, and background workers. Detect missed runs, silent failures, and queue backpressure with node-cron, Bull, and custom schedulers.