Back to Blog
2026-04-28 • 10 min read

Laravel Scheduler Monitoring: How to Catch Missed Tasks Before They Break Production

Laravel scheduler monitoring is one of those things many teams postpone until the first silent failure hurts. The app is online. The dashboard looks fine. No one is reporting errors. But behind the scenes, a scheduled command stopped running three days ago, invoices were not generated, cleanup jobs never fired, or reminder emails quietly disappeared.

That is the dangerous part: Laravel scheduled tasks often fail silently. Unless you actively monitor whether they ran, you may only discover the problem when customers complain or data starts drifting.

This guide explains why Laravel scheduler failures happen, why logs are not enough, and how to detect missed scheduled tasks with simple heartbeat monitoring.

The problem

Laravel makes scheduled tasks feel clean and simple.

Instead of managing many separate cron entries, you usually add one cron job to the server:

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

Then you define tasks inside app/Console/Kernel.php:

protected function schedule(Schedule $schedule): void
{
    $schedule->command('reports:send')->dailyAt('08:00');
    $schedule->command('subscriptions:sync')->hourly();
    $schedule->command('cleanup:old-sessions')->everyThirtyMinutes();
}

This is great for developer experience. But operationally, it creates a blind spot.

If the system cron stops calling schedule:run, none of your scheduled tasks run. If a command crashes before doing its work, it may only show up in a log file nobody reads. If a deployment changes paths, permissions, environment variables, or PHP versions, the scheduler can break while the main web app still works.

Your application may continue serving HTTP requests normally while scheduled work silently stops.

That is the core Laravel scheduler monitoring problem: uptime does not prove scheduled tasks are running.

Why it happens

Laravel scheduled jobs can stop for several reasons.

The most common one is that the server cron entry is missing, disabled, or running in the wrong directory. The Laravel scheduler depends on that single schedule:run command being executed every minute. If cron is not configured correctly, Laravel will not run scheduled tasks at all.

Environment differences are another frequent cause. A command that works from your terminal may fail under cron because cron has a smaller environment. The PATH may be different, the PHP binary may not be the same, or required variables from .env may not be loaded as expected.

Deployments can also break scheduled tasks. A release may move the app path, change file permissions, rotate symlinks, or briefly leave the scheduler pointing at an old directory. If you deploy with zero-downtime release folders, a cron command hardcoded to the wrong path can keep calling stale code.

Long-running commands can cause trouble too. Laravel has helpful scheduler features like withoutOverlapping(), but they can also hide problems if a lock gets stuck. One blocked task can prevent future executions from happening.

There are also application-level failures:

$schedule->command('billing:charge-renewals')->daily();

Maybe the command starts, but then fails because the payment provider API changed. Maybe it catches exceptions and logs them without failing the process. Maybe it exits successfully before doing the important part. From the outside, the server still looks healthy.

This is why Laravel scheduler monitoring needs to focus on the outcome that matters: did the task run successfully when expected?

Why it's dangerous

Missed scheduled tasks rarely look urgent at first. That is exactly why they are risky.

A failed queue cleanup may slowly fill storage. A missed billing sync may delay revenue collection. A failed reminder email task may reduce activation without triggering any obvious incident. A stale cache refresh may show outdated data. A missed subscription expiration job may leave users in the wrong state.

The impact depends on the task, but the pattern is usually the same:

  1. The task stops running.
  2. Nobody notices immediately.
  3. Data becomes stale or inconsistent.
  4. Users eventually notice the symptoms.
  5. The team has to reconstruct what happened.

For small teams and indie products, this can be especially painful. There may be no dedicated DevOps person watching logs. There may be no incident response process. The app can be “up” while important background work is broken.

Traditional uptime monitoring does not catch this. An uptime check can confirm that https://yourapp.com returns 200 OK. It cannot tell you whether php artisan subscriptions:sync ran at 03:00.

Error tracking helps, but only if the failure throws an exception and the exception is reported correctly. Logs help, but only if someone reads them or has alerts attached to them.

Laravel scheduler monitoring needs a direct signal from the scheduled task itself.

How to detect it

The simplest reliable pattern is heartbeat monitoring.

A heartbeat is a small HTTP request sent by your scheduled task when it runs successfully. A monitoring service expects that heartbeat within a defined time window. If the heartbeat does not arrive, you get an alert.

For example, if a Laravel command should run every hour, you configure a monitor that expects a ping every hour plus a small grace period. When the command completes successfully, it sends the ping. If the command does not run, hangs, crashes, or fails before completion, no ping is sent.

That missing ping becomes the alert.

This is different from checking whether the server is alive. It checks whether the expected work actually happened.

For Laravel scheduler monitoring, you can apply heartbeats at different levels:

  • Monitor the global scheduler with a ping after schedule:run.
  • Monitor individual important commands.
  • Monitor only successful completion, not just command start.
  • Use different heartbeat URLs for different critical tasks.

The most useful approach is usually to monitor important commands individually. If billing:charge-renewals fails, you want to know that exact task failed — not just that “something in the scheduler” might be wrong.

Simple solution (with example)

Imagine you have a Laravel command that sends daily reports:

$schedule->command('reports:send')->dailyAt('08:00');

Inside the command, you can ping a heartbeat URL after the work completes successfully.

A simple version using Laravel’s HTTP client:

use Illuminate\Support\Facades\Http;

public function handle(): int
{
    // Do the important scheduled work.
    // Example: generate and send reports.
    $this->sendDailyReports();

    // Send heartbeat only after successful completion.
    Http::timeout(5)->get('https://quietpulse.xyz/ping/YOUR_TOKEN');

    return self::SUCCESS;
}

The important detail is placement. Do not send the heartbeat at the start of the command if you want to detect failed completion. Send it after the task has done the work that matters.

For a command that should run hourly:

protected function schedule(Schedule $schedule): void
{
    $schedule->command('subscriptions:sync')
        ->hourly()
        ->withoutOverlapping();
}

The command can send a heartbeat when the sync finishes:

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;

class SyncSubscriptions extends Command
{
    protected $signature = 'subscriptions:sync';

    public function handle(): int
    {
        $this->info('Syncing subscriptions...');

        $this->syncSubscriptions();

        Http::timeout(5)->get('https://quietpulse.xyz/ping/YOUR_TOKEN');

        $this->info('Subscriptions synced.');

        return self::SUCCESS;
    }
}

You can also make the heartbeat URL configurable:

$pingUrl = config('services.scheduler_pings.subscriptions_sync');

if ($pingUrl) {
    Http::timeout(5)->get($pingUrl);
}

Then in config/services.php:

'scheduler_pings' => [
    'subscriptions_sync' => env('SUBSCRIPTIONS_SYNC_PING_URL'),
],

And in .env:

SUBSCRIPTIONS_SYNC_PING_URL=https://quietpulse.xyz/ping/YOUR_TOKEN

This keeps secrets and environment-specific monitor URLs out of your code.

Instead of building all alerting yourself, you can use a heartbeat monitoring tool like QuietPulse. Create a monitor for the scheduled task, set the expected interval, and ping https://quietpulse.xyz/ping/{token} after successful completion. If the Laravel task misses its window, QuietPulse can alert you through Telegram or webhooks.

Common mistakes

1. Monitoring only the server

Server metrics are useful, but they do not prove Laravel scheduled tasks are running. CPU, memory, disk, and HTTP uptime can all look normal while the scheduler is broken.

Use server monitoring for infrastructure health, but add task-level monitoring for scheduled work.

2. Sending the heartbeat too early

If you ping at the start of the command, the monitor only proves that the command started. It does not prove it finished.

For most scheduled tasks, the heartbeat should happen after the important work completes.

Bad pattern:

public function handle(): int
{
    Http::get('https://quietpulse.xyz/ping/YOUR_TOKEN');

    $this->processInvoices();

    return self::SUCCESS;
}

Better pattern:

public function handle(): int
{
    $this->processInvoices();

    Http::get('https://quietpulse.xyz/ping/YOUR_TOKEN');

    return self::SUCCESS;
}

3. Using one monitor for every task

A single “scheduler is alive” heartbeat is better than nothing, but it can hide failures in individual commands.

If billing sync fails but cleanup succeeds, a generic scheduler heartbeat may still arrive. Critical tasks deserve separate monitors.

4. Ignoring overlapping and stuck locks

Laravel’s withoutOverlapping() is useful, but stuck locks can prevent future runs. If a task never reaches the heartbeat, monitoring will catch that. Without monitoring, the task may silently stop executing.

If you use withoutOverlapping(), make sure the expected heartbeat interval accounts for the schedule and possible runtime.

5. Relying only on logs

Logs are valuable for debugging after an alert. They are not always good at creating the alert.

A log line saying “started report generation” does not help if no one reads it. A missing heartbeat is easier to reason about: the task did not report success within the expected window.

Alternative approaches

Heartbeat monitoring is not the only tool. It is one piece of a reliable setup.

Laravel has built-in scheduler output options:

$schedule->command('reports:send')
    ->daily()
    ->sendOutputTo(storage_path('logs/reports.log'));

You can also email output:

$schedule->command('reports:send')
    ->daily()
    ->emailOutputOnFailure('ops@example.com');

These are useful, especially during debugging. But they depend on failure output existing and email delivery working. They also do not always catch tasks that never started.

Application error tracking is another good layer. Tools like Sentry or Bugsnag can catch exceptions inside scheduled commands. But again, they work best when errors are thrown and reported. A task that never runs may produce no exception.

Queue dashboards can help if your scheduled task dispatches jobs. Horizon, for example, is great for Laravel queue visibility. But queue health and scheduler health are not the same. A scheduled command may fail before dispatching anything.

You can also create database audit rows:

DB::table('job_runs')->insert([
    'name' => 'reports:send',
    'finished_at' => now(),
    'status' => 'success',
]);

This is useful for internal visibility and history. But if you want external alerts when something goes missing, heartbeat monitoring is usually simpler.

A strong Laravel scheduler monitoring setup often combines several layers:

  • Cron configured correctly on the server.
  • Logs for debugging.
  • Error tracking for exceptions.
  • Queue monitoring for background workers.
  • Heartbeat monitoring for missed scheduled task completion.

Each layer answers a different question.

FAQ

What is Laravel scheduler monitoring?

Laravel scheduler monitoring means checking that your scheduled Laravel commands actually run when expected. The most reliable approach is to send a heartbeat after successful task completion and alert when that heartbeat is missing.

Does Laravel notify me when scheduled tasks fail?

Laravel has features for output and failure handling, but it does not automatically monitor every scheduled task from outside your app. If the system cron stops running schedule:run, Laravel may not get a chance to notify you.

Is uptime monitoring enough for Laravel scheduled tasks?

No. Uptime monitoring only checks whether your web app responds to HTTP requests. Laravel scheduled tasks can fail while the website remains online.

Should I monitor schedule:run or individual commands?

Both can be useful, but individual commands give better visibility. A global scheduler heartbeat tells you the scheduler is alive. Per-command heartbeats tell you whether important tasks completed successfully.

Where should I put the heartbeat ping in a Laravel command?

Usually near the end of the command, after the important work has completed successfully. If you ping at the beginning, you may miss failures that happen during execution.

Conclusion

Laravel’s scheduler is convenient, but convenience does not remove the need for monitoring. A single cron entry can control many important tasks, and those tasks can fail while the rest of your app looks healthy.

Good Laravel scheduler monitoring focuses on the signal that matters: did the scheduled task complete when expected?

For critical jobs like billing, reports, cleanup, imports, reminders, and syncs, add a heartbeat after successful completion. If the heartbeat goes missing, treat it as an early warning before users notice the damage.