[ GUIDE ]

How to Monitor Laravel Scheduled Tasks in Production

Scheduled tasks fail silently by default. Here's how to know when they do — and why.

QUICK ANSWER

How do I know if my Laravel scheduled tasks are running?

Laravel's scheduler has no built-in alerting for silent failures. You need one of three approaches: (1) a dead-man's-switch service (Healthchecks.io, Cronitor) called via pingOnSuccess, (2) Event listeners on ScheduledTaskStarting/Finished/Failed that log to your own system, or (3) an APM like NightOwl or Nightwatch Cloud that records every task invocation with duration, exit code, and failure context.

Updated · 2026-04-13

Why tasks fail silently

Laravel's scheduler runs as a single cron entry:

bash
* * * * * cd /srv/app && php artisan schedule:run >> /dev/null 2>&1

If that cron stops running — because cron is down, the filesystem is full, the deploy script broke the crontab, or schedule:run crashes before reaching your task — every scheduled task in your app stops. And you'll find out the next time a report is missing or a subscription didn't renew.

Option 1 — Dead-man's switch per task

Services like Healthchecks.io, Cronitor, and Oh Dear give you URLs that expect pings on a schedule. Miss a ping, get paged.

routes/console.phpphp
use Illuminate\Support\Facades\Schedule;

Schedule::command('app:generate-invoices')
    ->dailyAt('01:00')
    ->onOneServer()
    ->withoutOverlapping(10)
    ->pingOnSuccess('https://hc-ping.com/xxxxxxxx-invoices-start')
    ->pingOnSuccess('https://hc-ping.com/xxxxxxxx-invoices-done')
    ->onFailure(fn () => logger()->error('Invoices job failed'));

Works reliably. The downside: one URL per task (tedious at 20+ tasks), and you only learn that a task failed — not why, not the stack trace, not the duration trend.

Option 2 — Scheduler events

Laravel dispatches events for every scheduled task lifecycle. Listen and log to any destination.

app/Providers/AppServiceProvider.phpphp
use Illuminate\Console\Events\ScheduledTaskStarting;
use Illuminate\Console\Events\ScheduledTaskFinished;
use Illuminate\Console\Events\ScheduledTaskFailed;
use Illuminate\Support\Facades\Event;

public function boot(): void
{
    Event::listen(ScheduledTaskFinished::class, function ($event) {
        logger()->info('Scheduled task finished', [
            'task' => $event->task->command ?? $event->task->description,
            'runtime' => $event->runtime,
        ]);
    });

    Event::listen(ScheduledTaskFailed::class, function ($event) {
        logger()->error('Scheduled task failed', [
            'task' => $event->task->command ?? $event->task->description,
            'exception' => $event->exception->getMessage(),
        ]);
    });
}

Pipes through any Laravel log channel. Flexible, but you still need a destination that indexes, groups, and alerts on these log lines — raw logs in a file aren't a monitoring system.

Prevent overlap and split across servers

Two chainable calls every production app should use:

php
Schedule::command('reports:generate')
    ->hourly()
    // Only one server in the cluster runs it — needs Redis/Memcached cache
    ->onOneServer()
    // If the task is still running from last hour, skip this invocation.
    // The integer is a lock-timeout in minutes — CRITICAL: without it a crashed
    // task holds the lock forever and blocks every future run.
    ->withoutOverlapping(120);

What to alert on

In order of value:

  1. Scheduler didn't run at all — highest-priority. Use a dead-man's switch on the schedule:run command itself, not just individual tasks.
  2. Task failed — group by task name, alert on threshold (e.g. 3 failures in 1 hour), not every occurrence.
  3. Task duration spiked — the report job that used to take 30s now takes 8 minutes. Use p95 trending, not single-invocation thresholds.
  4. Task didn't run on expected cadence — hourly task missed an hour. Needs history, not just current-state.

THE EASY WAY

NightOwl records every scheduled task with full history

NightOwl's scheduled-task watcher logs every invocation with start time, end time, duration, exit code, and exception. You see per-task history (processed / failed / skipped), duration trending, and exception capture on one page — no custom event listeners, no separate service per task.

Alert channels (Slack, Discord, Email, Webhook) handle threshold-based alerts so a single flaky task doesn't spam your channel.

bash
composer require nightowl/agent
php artisan nightowl:install

Telemetry writes to your PostgreSQL. Works on every driver Laravel's scheduler supports.

Frequently asked questions

How do I know if my Laravel scheduled tasks are running?

Laravel's scheduler runs tasks via a single cron entry that calls schedule:run every minute. If that cron is broken, silently fails, or the box is down, your tasks stop running with no notification by default. Options: pingBefore/pingAfter to a dead-man's-switch service, Laravel's built-in events, or an APM that logs every task invocation.

Why did my Laravel scheduled task stop running silently?

Four common causes: (1) the cron service on the server is stopped, (2) the schedule:run command is crashing before it reaches your task, (3) another task is blocking the scheduler process, (4) the task has a withoutOverlapping lock that didn't release. You won't know unless you monitor invocations, not just the task code.

What's a dead-man's-switch for Laravel scheduled tasks?

A dead-man's switch is a service (Healthchecks.io, Cronitor, Oh Dear) that expects a ping on a schedule — if the ping doesn't arrive, it alerts you. Wire it to Laravel with ->pingOnSuccess('https://...'). The downside: you have to configure one endpoint per task, and you only learn that a task failed, not why.

How do I log Laravel scheduled task runs?

Hook into the scheduler's events: Event::listen(ScheduledTaskStarting::class) and ScheduledTaskFinished::class. These fire per task with duration and exit code. Log those to a table or send to an APM. Laravel Nightwatch and NightOwl both capture these events automatically with full history.

How do I prevent scheduled tasks from overlapping?

Chain ->withoutOverlapping() on the schedule definition. This uses the cache to hold a lock while the task runs. You can pass a timeout: withoutOverlapping(10) releases the lock after 10 minutes even if the process hangs — critical because otherwise a crashed task can block future runs indefinitely.

Should scheduled tasks run on one server or all servers?

Chain ->onOneServer() when running the scheduler on multiple machines behind a load balancer. It uses a distributed lock so only one server runs the task. Requires a shared cache driver (Redis or Memcached) — the default file cache doesn't work because it's per-server.

How do I alert when a scheduled task fails?

Use ->onFailure(fn() => ...) in the schedule definition, or listen to ScheduledTaskFailed events. For production, route alerts through a channel that groups noise — per-task threshold alerts (e.g., 3 failures in an hour) work better than per-invocation pages. NightOwl's alert channels handle this grouping automatically.

PRICING

Flat pricing. No event caps. No per-seat fees.

14-day free trial, no credit card. Your PostgreSQL, your data.

HOBBY

$5 /month

1 app · 14 days lookback · all Laravel events

TEAM

$15 /month

Up to 3 connected apps · unlimited environments · all Laravel events

AGENCY

$69 /month

Unlimited apps · unlimited agent instances · same flat rate at any traffic

Related