Skip to main content
All posts
· 5 min read

Why Generic Cron Monitors Miss Laravel Horizon Failures

Cronitor tells you a job ran. It can't tell you your Horizon supervisor is paused. Here's what's actually going on under the hood.

Most Laravel developers set up a cron monitor by pinging a URL every time a job runs. Cronitor, Better Stack heartbeats, or Forge Heartbeats – they all work the same way. The job fires, it hits a URL, the monitor marks it green.

The problem: that’s not how Horizon failures actually happen.

The real failure modes

When your Horizon setup breaks in production, it usually looks like one of these:

1. A supervisor process dies, but Horizon “stays running”

Horizon is a supervisor of supervisors. If you have two supervisor groups – say, one for default queue and one for emails – and one of them crashes, Horizon’s status still shows “running” because the other supervisor is alive. Your email queue is now dead. Generic monitors have no way to know this.

2. Horizon gets paused accidentally

php artisan horizon:pause is a valid command, sometimes run during deployments. If it never gets horizon:continue called after, your queues silently stop processing. No cron missed a heartbeat because the scheduled commands are separate from Horizon workers. Everything looks fine.

A related pattern is a Horizon config-environment mismatch — where the wrong supervisor config activates after a deploy because APP_ENV doesn’t match or the cached config is stale.

3. The failed job rate spikes gradually

Jobs fail. That’s normal. But when the failure rate climbs from 0.2/min to 8/min over two hours, you want to know before it becomes 40/min and your users start screaming. Generic monitors can’t see job failure rates – they only know if a URL was pinged.

What actually needs to be monitored

For a Laravel app with Horizon, you need to watch:

  • Per-supervisor status – not just “is Horizon running”
  • Failed jobs per minute – with a configurable threshold
  • Queue depths per queue – not just “is the queue being worked”
  • Oldest job age – a queue with 1 job aged 8 hours is worse than one with 500 fresh jobs
  • Paused state – explicitly tracked, not inferred

None of these are available to an HTTP ping monitor. They require reading Horizon’s internal Redis keys.

How Crontinel approaches this

Crontinel’s Laravel package reads Horizon’s Redis keys directly – the same data that the Horizon dashboard shows you, but piped into configurable alerts. It also hooks into Laravel’s ScheduledTaskFinished and ScheduledTaskFailed events to track every cron run with its exit code and duration.

The result: you get alerted when a supervisor goes down, not after a customer notices their invoice wasn’t sent.

The OSS package is free forever. If you want multi-app dashboards and a hosted view, that’s what the SaaS is for.

composer require crontinel/laravel
php artisan crontinel:install

See also

blog
Laravel Cron vs Queue Monitoring — What's the Difference?

Generic uptime monitors miss scheduler failures. Queue depth monitors miss Horizon supervisor death. Here's why you need both, and what each one actually covers.

blog
Why Generic Uptime Monitors Miss Laravel Queue Failures

Your uptime monitor shows green while your queue depth climbs to 5,000 and customers wait. Here's why HTTP pings can't see Laravel queue failures, and what actually works.

use cases
horizon:purge Not Cleaning Jobs? Detect Silent Failures

horizon:purge can exit with code 0 while Redis memory keeps climbing. Here is exactly how to tell if it is actually working — and how to get alerted when it isn't.

vs
Crontinel vs Cronofy: Scheduling API vs Laravel Job Monitoring

Cronofy is a scheduling and calendar API. Crontinel monitors whether your scheduled jobs actually ran. Here's where they differ and when you need both.