# laravel-schedule-monitor: a log table so a quiet cron failure is no longer invisible

> This Spatie package records every start, finish, failure and skip of a Laravel scheduled task in a database table, and can sync the schedule with Oh Dear so a job that does not run on time produces a notification. This looks at the integration points, the configuration, and where the approach stops.

**spatie/laravel-schedule-monitor** — Monitor scheduled tasks in a Laravel app

- Repository: https://github.com/spatie/laravel-schedule-monitor
- Website: https://spatie.be/open-source
- Stars: 1,003 · Forks: 75
- Language: PHP
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/spatie-laravel-schedule-monitor

## A cron job that stops is invisible by default

Laravel's scheduler runs your scheduled tasks from a single cron entry that fires once a minute. That design is the whole point, one entry rather than a hundred, and it is also the problem. If a task stops running, because the command inside it broke, because a host rebooted and the entry was never reinstalled, or because the scheduler itself is not being invoked, there is nothing in the application that says so. The task does not fail loudly, it simply does not happen.

This package closes that gap. It monitors the Laravel schedule and writes an entry to a log table in the database each time a scheduled task starts, ends, fails or is skipped. The `list` command then lets you check when the scheduled tasks have been executed.

The four events are worth noting, because they are what makes the record useful. A start without an end is a task that hung or was killed. A fail is a task that threw. A skip is a task the scheduler decided not to run, which on a conditional schedule is the interesting case, because a skip can be entirely correct or a symptom of a broken condition.

The audience is a Laravel team that has been paged by a job that quietly stopped, and the value is a table you can query. That is a different proposition from an external monitoring agent that pings a URL, and for many teams it is the better one, because the record lives next to the data it describes.

## Composer, two vendor:publish calls, and a migration

Installation is the standard Laravel package sequence.

```bash
composer require spatie/laravel-schedule-monitor
```

The database has to be prepared, and the documentation is explicit that you must publish and run the migrations:

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

And the config file is published separately:

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

Two different tags, two separate commands, and the migrations have to run before the package is useful, since the log is a database table. That is a normal Laravel package workflow rather than something unusual, but it is the step people skip.

There is also a version note worth knowing before you start. If you need Laravel 8 support, you can install v2 of the package with `composer require spatie/laravel-schedule-monitor:^2`. So the package supports more than one Laravel generation through more than one branch, and a long-lived application pinned to an older framework version is on an older branch of this package too.

## The config file, and the three values that matter

The published config is a single PHP array, and most of it is about the Oh Dear integration, but three entries are about the package itself.

The first is retention:

```php
    'delete_log_items_older_than_days' => 30,
```

with the comment explaining that after a while the `monitored_scheduled_task_log_items` table might become big, and that you should use Laravel's pruning command to delete old `MonitoredScheduledTaskLogItem` models. Thirty days is a starting point, not a recommendation, and the pruning is something you have to wire up.

The second is the display format for the dates the package prints in its command output:

```php
    'date_format' => 'Y-m-d H:i:s',
```

The third is the model binding, and this is the extension point:

```php
    'models' => [
        'monitored_scheduled_task' => Spatie\ScheduleMonitor\Models\MonitoredScheduledTask::class,
        'monitored_scheduled_log_item' => Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem::class,
    ],
```

Both models need to extend the package's own classes. That is the seam a project uses to add its own columns to the log, for example a tenant identifier or a correlation id, so the log can be sliced the way the rest of the application's data is. It is a small configuration, but it is the difference between a log table and a log table you can query for your own purposes.

## The Oh Dear sync, and the queued ping that carries it

The package can also sync your schedule with Oh Dear, and Oh Dear sends a notification whenever a scheduled task does not run on time or fails. The configuration for that is the larger part of the file, and it is a queued job with several careful settings.

The ping is dispatched as a queued job so that scheduled jobs stay as short as possible, with the queue named in the configuration:

```php
    'queue' => env('OH_DEAR_QUEUE'),
    'ping_oh_dear_job' => Spatie\ScheduleMonitor\Jobs\PingOhDearJob::class,
    'retry_job_for_minutes' => 10,
    'silence_ping_oh_dear_job_in_horizon' => true,
```

The retry window of ten minutes is what stops a backlog of pings from a queue outage from firing hours later, and the Horizon silencing option adds those jobs to Horizon's silenced jobs so a queue backup does not produce a wall of failures.

Two timing settings decide what counts as a problem. `grace_time_in_minutes` is the amount of minutes a scheduled task is allowed to run before it is considered late, five by default. `send_starting_ping` sends the start of a scheduled job, and the comment notes this is not needed for notifications to work correctly, so it is off by default and exists for troubleshooting.

The transport settings are the rest: `endpoint_url` for the endpoint to ping, `api_url` defaulting to `https://ohdear.app/api/`, an `api_token` and a `monitor_id` for the monitor you are syncing to, and a `retry_delay_ms` of ten seconds between retry attempts when a ping fails.

## Debug logging that shows the TCP breakdown

There is a dedicated section for the case where pings to Oh Dear intermittently time out, and the solution is an environment variable rather than a config edit:

```env
OH_DEAR_DEBUG_LOGGING=true
```

When it is enabled, each failed ping writes a `Log::warning` with the full request details, and the list of what that includes is unusually specific: a cURL timing breakdown covering DNS lookup, TCP connect, TLS handshake and time-to-first-byte, connection information with the resolved IP and the local IP, and the server response if one was received.

That level of detail matters because the failure mode it targets, an intermittent timeout between an application server and a third party endpoint, is exactly the kind of problem that a generic ping failure message cannot diagnose. Knowing whether the DNS lookup was slow or the TLS handshake was the bottleneck is the difference between a five minute fix and an afternoon. The `retry_delay_ms` setting of ten seconds is what determines how quickly a failed ping is retried while this is happening.

## Where laravel-schedule-monitor is the wrong tool

Three limits are clear, and they come from what the package observes.

The first is that it observes execution, not correctness. The log records that a task started, ended, failed or was skipped. It does not know whether the report the task generated had the right numbers in it. For a job whose value is in what it produced rather than in whether it ran, the monitor tells you nothing you did not already get from the job's own output.

The second is the dependency on the database. The record is written to a table in your application's database, so the thing that tells you the scheduler is broken depends on the same database the scheduler's tasks probably use. A database outage and a scheduler outage produce the same symptom, which is no symptom at all. An external monitor that checks a URL and tells you when the page goes stale catches the case where your whole stack is down; this one does not.

The third is the notification path. The alerting in this package is Oh Dear, which is a hosted service and a paid one. A team that has its own alerting stack, a Prometheus and an on-call rotation, would use the log table and their own tooling, and the Oh Dear configuration becomes dead weight. The package is still useful in that shape, but the readme's framing leads with the hosted option.

On maintenance, the record is regular. v4.2.0 was published on 2026-02-22, v4.3.0 on 2026-03-25, and v4.3.1 on 2026-06-25, with the last push to the repository on 2026-06-25. The licence is MIT, with the licence text in `LICENSE.md` at the root, and there is an `UPGRADING.md` in the repository for the version jumps, which is the file to read before you move between major lines.

## Conclusion

laravel-schedule-monitor fits a Laravel team that has been caught by a scheduled job silently not running, and that wants a cheap record of what actually executed rather than a monitoring agent. It does not fit a team that needs alerting without a third party, since the notification path in this package is Oh Dear, and it is the wrong choice for a job that runs but does the wrong thing, because the package records that the task started and ended, not that it succeeded in its intent. Before you adopt it, decide who prunes the log table, since the package logs every run of every task, and check the Laravel version you are on, because the Laravel 8 line lives in the v2 branch.

## FAQ

### What does spatie/laravel-schedule-monitor record?

It writes an entry to a log table in the database each time a scheduled task starts, ends, fails or is skipped, and the `list` command shows when the scheduled tasks have been executed.

### How do I install the schedule monitor in Laravel?

Run `composer require spatie/laravel-schedule-monitor`, then publish and run the migrations with the `schedule-monitor-migrations` tag, and publish the config with the `schedule-monitor-config` tag. For Laravel 8, install v2 with `composer require spatie/laravel-schedule-monitor:^2`.

### How do I keep the monitored task log table small?

The published config sets `delete_log_items_older_than_days` to 30 and the comment instructs you to use Laravel's pruning command to delete old MonitoredScheduledTaskLogItem models. Without pruning configured, the log items accumulate.

### What does the Oh Dear integration do?

It syncs your schedule with Oh Dear, which notifies you when a scheduled task does not run on time or fails. The ping is sent as a queued PingOhDearJob on the queue named in the config, with a ten minute retry window, optional Horizon silencing, and a grace_time_in_minutes value of five by default.

### How do I debug failed pings to Oh Dear?

Set `OH_DEAR_DEBUG_LOGGING=true` in the environment. Each failed ping then logs a warning with the request details, a cURL timing breakdown of DNS lookup, TCP connect, TLS handshake and time-to-first-byte, the resolved and local IP addresses, and the server response if one arrived.

## Sources

- [License: MIT](https://github.com/spatie/laravel-schedule-monitor/blob/main/LICENSE)
- [Project website](https://spatie.be/open-source)
- [README](https://github.com/spatie/laravel-schedule-monitor/blob/main/README.md)
- [Releases](https://github.com/spatie/laravel-schedule-monitor/releases)
- [spatie/laravel-schedule-monitor on GitHub](https://github.com/spatie/laravel-schedule-monitor)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spatie-laravel-schedule-monitor
