For the first year of production, we thought we had monitoring figured out.
Horizon dashboard was green. Laravel Pulse showed healthy throughput. If the scheduler ran, we’d see the log lines. If a job failed, Horizon surfaced it.
Then a payment reconciliation job stopped running. The cron entry was still in crontab. The scheduler ticked every minute. The command just… didn’t finish. Horizon never knew about it — the job never made it to a queue.
We had observability (logs, metrics, a dashboard). What we didn’t have was monitoring: an independent check that told us “something that should be running, isn’t.”
This is the monitoring vs observability divide — and most Laravel teams get it backwards.
The Short Version
| Monitoring | Observability | |
|---|---|---|
| Question | ”Is it working?" | "Why did it break?” |
| Trigger | Alert when something stops | Debug after something breaks |
| Perspective | External (heartbeat, uptime) | Internal (logs, traces, metrics) |
| Tooling | Cron monitoring, status pages | Laravel Pulse, Telescope, OpenTelemetry |
| Failure mode | False negatives (silent outages) | Alert fatigue (everything looks “interesting”) |
You need both. But they serve fundamentally different roles.
Why Monitoring Comes First
Monitoring answers one binary question: “Did X happen by Y time?”
If my php artisan reports:send runs at 8 AM every day, monitoring checks: Was a heartbeat received by 8:05 AM?
That’s it. No traces. No log analysis. No querying a data store. A simple yes/no.
The value is that this check is independent of the system being monitored:
- If Redis goes down and Horizon stops processing — monitoring catches it
- If the scheduler process is killed by OOM — monitoring catches it
- If a deploy script misses the cron restart — monitoring catches it
- If an artisan command hangs silently instead of crashing — monitoring catches it
In every case above, your observability stack (logs, Pulse, Telescope) is either unavailable or never recorded the failure. The process that would have written the log line never got to run.
Where Observability Shines
Once monitoring tells you something is wrong, observability tells you what and where.
When Laravel Pulse reports elevated queue throughput but Horizon shows high wait times, you need traces to find the bottleneck. When Telescope surfaces repeated failures for the same job, you need the logged payload and stack trace.
Observability is essential for:
- Root cause analysis — Why did the job fail? SQL deadlock? Memory exhaustion? Third-party API timeout?
- Performance trends — Queue latency creeping up week over week
- Capacity planning — Which queues need more workers?
- Debugging production issues — What was the exact state when the failure occurred?
The Trap: Over-Indexing on Observability
Here’s where most teams go wrong.
Laravel makes observability easy. Pulse dashboard? Five-minute install. Telescope? One composer command. You get a beautiful UI showing throughput, failed jobs, slow queries, and request metrics.
It feels like monitoring. It isn’t.
The trap: observability tools only tell you about events they witness. If Pulse never received data because the scheduler died before Pulse could record the tick, Pulse shows nothing unusual. The dashboard stays green.
We call this the silent blank — your observability platform shows no data because the thing that generates the data didn’t run.
External monitoring (a heartbeat, a cron monitoring check) is the only way to distinguish between “nothing unusual happened” and “nothing happened at all.”
What the Split Looks Like in Practice
A production Laravel app should have:
Monitoring layer (binary, external):
- Heartbeat check on the scheduler (
php artisan schedule:run) - Heartbeat check on Horizon (
php artisan horizon:status) - HTTP endpoint monitoring for web-facing services
- Uptime check on the queue worker process
Observability layer (rich, internal):
- Laravel Pulse for aggregate metrics
- Laravel Telescope for request/job debugging (non-production or sampled)
- Log aggregation (Laravel’s logging stack + external sink)
- Redis metrics for queue depth and wait times
The monitoring layer pages you at 3 AM. The observability layer helps you understand what happened when you open your laptop.
A Concrete Example
Here’s the setup that finally stopped the silent reconciliation failures:
Scheduler runs: php artisan schedule:run --no-interaction
↓ Crontinel heartbeat ping (every run)
Horizon runs: php artisan horizon
↓ Crontinel heartbeat ping (process alive)
Payment reconciliation: scheduled: php artisan reports:send --queue=payments
↓ Crontinel heartbeat ping (command completed)
Three independent heartbeat pings. Each goes to an external service that doesn’t depend on the Laravel application stack. If any heartbeat stops arriving:
Crontinel → Slack → "reports:send heartbeat not received (expected by 08:05)"
That’s monitoring. It takes seconds to configure. It catches failures that Pulse never sees.
Then, to debug why reports:send hung: Pulse shows queue latency at time of incident. Telescope shows the last successful execution. Logs show the OOM killer’s intervention. The server’s dmesg confirms the memory pressure.
That’s observability. It’s indispensable — but it only helps after monitoring tells you something is wrong.
What Most Laravel Teams Miss
The single biggest gap I see: teams build elaborate observability stacks but have zero external monitoring.
- Pulse + Telescope + Grafana + Loki
- Horizon dashboard + Redis exporter
- Custom logging pipelines
And then a deploy caches the config with php artisan config:cache, which resets the schedule cache, and the 8 AM reports job quietly disappears from the schedule:run output. No alert fires. The dashboard stays green. The first sign of trouble? A customer email three days later.
External monitoring — a heartbeat, a cron monitoring ping — is the only safety net for the scenarios where your observability stack itself goes dark.
The Crontinel Approach
Crontinel is built around this distinction.
The monitoring layer (heartbeat checks, schedule monitoring, queue worker monitoring) runs independently of your Laravel app. It receives pings from your scheduler, Horizon, and artisan commands. If a ping doesn’t arrive on schedule, it alerts — regardless of what’s happening inside your stack.
The observability layer is whatever you choose: Pulse, Telescope, OpenTelemetry, Grafana. Crontinel doesn’t replace it. Crontinel makes sure you know to go look at it.
Crontinel (monitoring): "schedule:run didn't fire at 08:00"
↓
You check Pulse (observability): no data after 07:58
↓
You check server logs: process killed by OOM at 07:59
Without monitoring, you’d never know the data was missing from Pulse.
The Takeaway
If you have a production Laravel app with scheduled tasks, here’s your priority order:
- Heartbeat monitoring on the scheduler, Horizon/queue workers, and any critical artisan commands
- Alerting when heartbeats stop — Slack, PagerDuty, email, whatever reaches you
- Observability — Pulse, Telescope, or your stack of choice — to debug when alerts fire
Most teams build from 3 backward. Build from 1 forward. Monitoring first, observability second.
They’re complementary. They’re not interchangeable. And the difference between them is the difference between catching a failure at 8:01 AM and discovering it from a customer complaint at 3 PM.