Cron doesn't care if your job succeeds. It runs the command at the scheduled time and moves on — no alert, no notification, nothing. Most of the time that's fine, because most of the time the job either runs and works, or runs and throws an error your logging already catches.
The failure mode that actually hurts is the one where the job doesn't run at all. The server rebooted and cron never restarted. Someone edited the crontab and dropped a line. The disk filled up and cron couldn't write its lockfile. A deploy overwrote the schedule. In every one of these cases there's nothing to log, because nothing happened. The job just... didn't.
If that job is a billing sync, a report generator, a backup, or a data import, you don't find out until a client asks where their invoice is.
The fix: heartbeat monitoring (a dead man's switch)
The pattern that catches this is simple and old: instead of watching the job for failure, you watch for the job's check-in. Your job pings a URL every time it runs. A monitoring service expects that ping on a schedule. If the ping doesn't show up in the expected window, it alerts you — the same idea as a dead man's switch on a train, or a "still alive" heartbeat between two systems.
It flips the monitoring question from "did this fail?" to "did this happen at all?" — which is the question error logging can't answer, because error logging only runs if the job runs.
What a good cron monitor needs
A few things separate a monitor that's actually useful from one you'll end up muting:
- A real schedule definition — either a cron expression (
0 2 * * *) or a plain frequency, so the monitor knows exactly when to expect a ping, not just "sometime today." - Grace time and missed-run tolerance — a job that's 90 seconds late because of normal server load shouldn't page anyone. Good tools let you set how many misses to tolerate or how late a job can be before alerting.
- Start/finish/fail lifecycle, not just one ping — knowing a job started and never finished is a different problem than it never starting at all. Splitting the ping into
/started,/finished, and/failedtells you which one happened. - A real alerting path — Slack, email, PagerDuty, webhooks — wherever your team actually looks, not just a dashboard nobody checks.
Avoiding alert fatigue
The fastest way to make cron monitoring useless is to alert on every 30-second blip. If your team starts ignoring cron alerts because half of them are noise, you've lost the whole point. Set grace windows that match reality — a nightly job that occasionally runs 5 minutes late because of load isn't an incident, three missed nights in a row is.
Setting it up (Laravel example)
If you're already scheduling jobs through Laravel's scheduler, wiring in a monitor shouldn't mean rewriting anything. With DownCTL's Laravel package, it's one method chained onto the existing schedule:
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:daily')
->dailyAt('06:00')
->cronMonitor(config('downctl.monitors.reports_daily'));
That automatically pings /started, /finished (with measured runtime), or /failed depending on how the command exits.
Outside Laravel, the ping URL is just an HTTP request, so it works from any script or language — crontab, a shell script, a CI job, whatever's running the work:
#!/bin/sh
curl -fsS "$DOWNCTL_PING_URL/started"
/usr/bin/php /var/www/scripts/import.php
if [ $? -eq 0 ]; then
curl -fsS "$DOWNCTL_PING_URL/finished"
else
curl -fsS "$DOWNCTL_PING_URL/failed"
fi
Where it fits with everything else
Cron monitoring answers one specific question: did the background work happen? It's not a replacement for uptime monitoring (is the site reachable), error tracking (did something throw), or server monitoring (is the box under pressure) — it's the piece that catches the gap between all of them, where nothing crashed and nothing logged, because nothing ran.
If you're managing scheduled jobs across more than one site — client imports, nightly reports, billing syncs — DownCTL's cron job monitoring covers this alongside uptime, SSL, API, and server monitoring in one dashboard, so a missed job shows up next to everything else instead of living in its own separate tool. Setup details, including the Laravel and plain-PHP integration, are in the docs.