← Blog  ·  October 2026

Laravel Cron Job Monitoring: Alerts When Scheduled Commands Stop Running

Laravel's task scheduler is elegant — until php artisan schedule:run stops running and nobody notices. Here's how to add heartbeat monitoring to your Laravel scheduled commands in under 10 minutes.

Laravel's built-in task scheduler lets you define all your scheduled jobs in App\Console\Kernel.php and drive everything from a single crontab entry. It's clean, expressive, and easy to maintain. But it shares the same fundamental weakness as every cron-based system: when it stops working, there is no alert. The logs go quiet. The jobs don't run. And you find out days later when a customer asks why their weekly report never arrived.


How Laravel scheduling works (and where it breaks)

The entire Laravel scheduler depends on one crontab entry running every minute on your server:

* * * * * cd /var/www/myapp && php artisan schedule:run >> /dev/null 2>&1

When this fires, Laravel checks your Kernel.php schedule definition, determines which commands are due, and runs them. When it doesn't fire — because the server was patched and rebooted, the deploy wiped the crontab, a permissions change broke php, or the path to your app changed — nothing runs and nothing tells you.

Common reasons Laravel scheduled commands silently stop

The real problem isn't that things break — it's that you find out too late. Without monitoring, a broken scheduler is invisible until a user reports missing data, a missed billing run creates underpayments, or a backup that never ran means data is unrecoverable.

Option 1: Laravel's built-in onFailure hook

Laravel provides lifecycle hooks for scheduled commands. You can chain onFailure() to send an alert when a command throws an exception:

$schedule->command('reports:generate')
    ->daily()
    ->onFailure(function () {
        // Send a Slack/email notification
        Notification::route('mail', 'team@example.com')
            ->notify(new ScheduledCommandFailed('reports:generate'));
    });

This is useful — but it only catches exceptions thrown inside the command. It does nothing when the scheduler itself stops running. A broken crontab or a missing php binary means schedule:run never executes at all, so no hooks fire.

Option 2: Pipe output to a log and set up alerts

You can capture command output and monitor for stale log files:

$schedule->command('reports:generate')
    ->daily()
    ->appendOutputTo(storage_path('logs/scheduler.log'));

Then watch scheduler.log for recent writes with a file-age alert. This is better than nothing, but it requires another monitoring layer that is itself a single point of failure.

Option 3: Heartbeat monitoring (recommended)

The most reliable approach is a dead man's switch: your scheduled command pings a monitoring URL after it runs successfully, and if the ping doesn't arrive within the expected window, an alert fires.

This inverts the failure model. Instead of waiting for an exception, the monitoring service expects a ping. Silence is the alert condition. This catches every failure mode: broken crontab, crashed server, OOM kill, permission error, and exceptions thrown inside the command.

Setting up heartbeat monitoring in Laravel

Add a ping at the end of your scheduled command using Laravel's thenPing() hook:

// In App\Console\Kernel.php
$schedule->command('reports:generate')
    ->daily()
    ->thenPing('https://cronwatch.example.com/api/heartbeat/YOUR_TOKEN');

Or add the ping inside the command itself for more control:

// In App\Console\Commands\GenerateReports.php
public function handle(): int
{
    // ... your command logic ...

    // Ping heartbeat monitor on success
    Http::get('https://cronwatch.example.com/api/heartbeat/YOUR_TOKEN');

    return Command::SUCCESS;
}

The monitoring service then watches for incoming pings. If your daily command doesn't ping within 24 hours (plus a configurable grace period), you get an alert — email, webhook, or both.


What to monitor in a Laravel application

Most Laravel apps accumulate scheduled tasks over time. Here are the commands worth monitoring with heartbeats:

Command type Why it matters if it stops Suggested grace period
Database backups Silent data loss if undetected for days +2 hours
Billing / invoice generation Revenue impact, customer trust issues +1 hour
Email dispatch / queued notifications Users don't receive time-sensitive emails +30 minutes
Report generation Stale dashboards; customers notice immediately +1 hour
Cache warming Performance degradation under load +15 minutes
Cleanup / pruning jobs Storage fills up, tables grow unbounded +4 hours
Subscription status sync Incorrect access grants or revocations +1 hour

Monitoring the scheduler entry itself

For the top-level schedule:run entry — the one that runs every minute — you want to know immediately if the crontab breaks entirely. Add a dedicated heartbeat command:

// In Kernel.php — runs every 5 minutes as a scheduler health check
$schedule->call(function () {
    Http::get('https://cronwatch.example.com/api/heartbeat/SCHEDULER_HEALTH_TOKEN');
})->everyFiveMinutes();

This pings the monitoring service every 5 minutes. If it goes quiet for more than 10 minutes (using a 5-minute grace period), your scheduler has stopped running — something you'll know about immediately rather than when a daily backup is discovered missing.

Using Laravel HTTP client for pings

Laravel's Http facade (backed by Guzzle) is the cleanest way to send heartbeat pings. Since pings are fire-and-forget, suppress exceptions so a monitoring service being down doesn't break your command:

use Illuminate\Support\Facades\Http;

// Suppress exceptions — a heartbeat ping should never break the command
rescue(fn() => Http::timeout(5)->get($heartbeatUrl), report: false);

The rescue helper catches any exception and optionally reports it to your exception handler without re-throwing it. Heartbeat pings should never be a point of failure for the job itself.

Handling Laravel Forge and server management tools

If you use Laravel Forge, Ploi, or Envoyer to manage your server, these tools have their own cron management interfaces. The best practice is to manage your scheduler crontab entry through the tool rather than adding it manually — otherwise a sync will wipe it.

In Laravel Forge: Server → Scheduler → Add Scheduled Job

Command: php /var/www/your-app/artisan schedule:run
Frequency: Every minute

Forge also supports per-command scheduling from its UI, but the standard approach (one crontab entry + Kernel.php definitions) is more maintainable and easier to test locally.


Testing your scheduler locally

Before adding heartbeat monitoring, verify your commands actually run as expected. Laravel provides a useful testing method:

// In a test
$this->artisan('schedule:run');

// Or to test a specific command in isolation
$this->artisan('reports:generate')->assertExitCode(0);

For end-to-end smoke-testing the scheduler in production, run it manually and watch the output:

php artisan schedule:run --verbose

The --verbose flag shows which commands are due and whether they ran successfully. This is useful for debugging why a command isn't running when expected.


🔔 Know immediately when your Laravel scheduler stops running

CronWatch sends you an alert the moment a scheduled command misses its heartbeat ping — before your users notice. Add one line to your Laravel command and get email alerts automatically.

Get Early Access to CronWatch → Free Cron Expression Parser

Summary: Laravel scheduler monitoring checklist

  1. Add a top-level heartbeat (pings every 5 min) to detect when schedule:run stops entirely
  2. Add per-command heartbeats to critical commands (billing, backups, reports)
  3. Use thenPing() for simple commands, or inline Http::get() for more control
  4. Suppress ping exceptions with rescue() — a heartbeat failure must not break the job
  5. Manage the crontab entry through Forge/Ploi/Envoyer if you use those tools
  6. Test with php artisan schedule:run --verbose before adding monitoring

The hardest part of Laravel cron monitoring isn't the implementation — it's remembering to set it up. Most outages happen because heartbeat monitoring was "something we'd add later." Add it now, before the silent failure that causes a data incident.


← Back to all posts