Every Laravel app accumulates scheduled jobs: backups, report emails, cache
warmups, cleanup tasks, data syncs. And every one of them fails the same way —
silently. schedule:run exits 0 whether your backup ran or not. If the
server’s cron entry disappears, if the job throws, if it hangs on a lock —
nothing happens. No error page, no failed request, no log entry you’d ever
look at. The absence of a thing is invisible.
The classic story: a backup job dies in March, and you learn about it in September, on the day you actually need the backup.
What Laravel gives you out of the box
Laravel is honest about this problem — the scheduler has hooks for it:
$schedule->command('backup:run')
->daily()
->onFailure(fn () => /* notify someone */)
->thenPing('https://example.com/ping/backup');
Two limitations, though.
First, onFailure only fires when the task runs and fails. It cannot
tell you about the task that never started: crontab wiped by a server
migration, a supervisor that stopped restarting, a deploy that dropped
routes/console.php changes. The most dangerous failure mode is the one
where your code never executes — and in-process hooks can’t see it by
definition.
Second, thenPing solves that (it’s a dead man’s switch: an external
service alerts you when pings stop arriving), but it’s manual. You
create a check in some dashboard, copy its URL, paste it into the code, and
repeat for every task. Twenty tasks, twenty copy-pastes. Then someone changes
daily() to twiceDaily() in code and forgets to update the expected
schedule in the dashboard — now you get false alerts, or worse, none.
One line per task, schedule included
This is the problem CronAlive was built around. Install the SDK:
composer require cronalive/laravel
Two env vars:
CRONALIVE_PING_DOMAIN=https://ping.cronalive.com
CRONALIVE_PING_KEY=<your project ping key>
And then, per task, one method call — no dashboard round-trip:
$schedule->command('backup:run')
->dailyAt('03:30')
->pingCronaliveSlug('backup', graceSec: 1800, tags: ['backups', 'prod']);
On the first ping the check creates itself, and — this is the part that
matters — it reads the schedule off your scheduled task. ->dailyAt('03:30')
becomes cron=30 3 * * *, the task’s timezone travels with it, graceSec
sets how late a ping may be before alerting, tags group checks on the
dashboard. The schedule lives in your code and only in your code; the
monitoring can’t drift out of sync with it, because it was never retyped
anywhere.
| Your task | What the check learns |
|---|---|
->everyFiveMinutes() | cron=*/5 * * * * |
->dailyAt('03:30')->timezone('Europe/Berlin') | cron=30 3 * * *, tz=Europe/Berlin |
->everyThirtySeconds() | period=60 (see gotchas) |
The macros also signal the run itself: /start before the job, and after
it either a success ping (the plain check URL — success is the default
case, so it has no suffix) or /fail. That gets you three things for
free: alerts on jobs that
started but never finished (hung on a lock, killed by OOM), alerts on
jobs that exited non-zero, and a run-duration chart per job — so you notice
the backup that quietly went from 4 minutes to 40 before it becomes the
backup that overlaps its next run.
Pings are fire-and-forget and never throw. If the monitoring service is unreachable, your backup still runs — monitoring must not break the thing it monitors.
A real production example
Our own dogfood project runs 47 scheduled tasks through this — invoicing,
exports, queue maintenance, certificate renewals. The whole migration was
one method call per task in Kernel.php, and the checks appeared on the
dashboard by themselves on the next scheduler tick, each with its own cron
expression, timezone and grace already filled in.
Since then it has caught exactly the failures the in-process hooks can’t:
a task that stopped being scheduled at all after a refactor, and a nightly
job that hung on a stale cache lock — visible as a /start with no finish,
alert in Telegram before breakfast.
One of those tasks is the certificate renewal, and it marks the limit of what heartbeats can tell you. A green check proves the renewal ran — not that the new certificate ever reached the edge. A reload that quietly failed leaves you with a happy scheduler and a site that stops loading for everyone in three weeks. Same shape as the original problem, one level up: the job succeeded, the outcome is false.
That question is answered from the outside — an HTTP check against the public URL, run from several regions, watching the status code, the response time and the expiry date of the certificate that is actually on the wire. It is the natural other half of job monitoring: heartbeats watch the work, HTTP checks watch the result. Both live on the same dashboard here, which is mostly a comfort at 3 AM — one place to look, one set of alert channels to keep configured.
Gotchas worth knowing
Creating is not updating. Schedule parameters apply only when the check is first created. After that, pings just count — a deploy can never silently rewrite a schedule someone tuned in the dashboard. Changed the schedule in code? Update the check in the dashboard, or delete it and let the next ping recreate it.
->when() / ->skip() filters stop the pings too, so a skipped run
looks exactly like a missed one. Pause the check for expected skip windows,
or move the condition inside the job.
Sub-minute tasks (everyThirtySeconds() and friends) are monitored as
“pings at least once a minute” — Laravel keeps the real interval outside
the cron expression, and CronAlive’s shortest period is 60 seconds. True
for such a job, and it never false-alarms.
Grace has a floor of 30 seconds. Schedulers start jobs with a few seconds of jitter; a tighter grace would turn that jitter into false alerts, so anything lower is raised to 30 rather than refused.
Alternatives
Fairness section. Healthchecks.io is the
best-known dead man’s switch — clean, open source, self-hostable; you’ll
be managing check URLs by hand or writing your own provisioning against
its API, and uptime monitoring for HTTP endpoints is out of scope. Cronitor
and Dead Man’s Snitch are solid too, with the same manual-wiring caveat
for Laravel. If you only have two or three tasks and don’t mind the
copy-paste, thenPing plus any of them is a perfectly good setup.
CronAlive’s angle is the Laravel-native auto-provisioning above, plus the HTTP and certificate side in the same dashboard, and alerts via Telegram, Slack, Discord, email or webhooks. The free plan covers ten checks with no card — enough to wire up the jobs you actually worry about and see which of them have been failing.
Questions, bug reports, scheduler war stories — write to support@cronalive.com. If your scheduler has ever been dead for months without anyone noticing, we would like to hear how you found out.