Your scheduler failed at 3 AM. When would you find out?
schedule:run exits 0 whether your
backup ran or not. If the cron entry disappears, if the job
hangs on a lock, if a deploy drops the task — there is no
error page, no failed request, no log line you would ever
look at. The absence of a thing is invisible.
CronAlive watches the scheduler from the outside and tells you when a run stops arriving. One method call per task, and the schedule comes from your code.
What Laravel gives you, and where it stops
Laravel is honest about the 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.
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.
thenPing solves that, but it
is manual. It is a dead man's switch: an external
service alerts you when pings stop arriving. 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() and forgets to
update the dashboard — now you get false alerts, or worse,
none.
One line per task, schedule included
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. The schedule lives in your code and only in your code; the monitoring cannot drift out of sync with it, because it was never retyped anywhere.
| Your task | What the check gets |
|---|---|
->everyFiveMinutes() | cron=*/5 * * * * |
->hourly() | cron=0 * * * * |
->dailyAt('03:30') | cron=30 3 * * * |
->dailyAt('03:30')->timezone('Europe/Berlin') | the same plus tz=Europe/Berlin |
->everyThirtySeconds() | period=60 — see the caveats |
pingCronaliveSlug('etl', graceSec: 1800) | grace=1800 |
pingCronaliveSlug('etl', tags: ['etl', 'prod']) | tags=etl,prod |
The timezone is the task's own if it has one, otherwise your
app.timezone. Without
graceSec the project default
applies. Prefer addressing an existing check by id? Use
pingCronalive('<uuid>').
Three things you get for free
Hung jobs
The macros send /start before
the job. A start with no finish is a job stuck on a lock or
killed by OOM — invisible to any in-process hook.
Non-zero exits
The success ping goes out only when the job exits 0. A
failed job sends /fail and
alerts immediately, without waiting out the grace period.
Run duration
A 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 never hold your schedule
The macros do not use Laravel's
pingBefore /
thenPing /
pingOnFailure. Those callbacks
swallow errors, but they wait on Laravel's default scheduler
client — 10 s to connect, 30 s in total, per signal, so up to
90 s for a job with three of them. Scheduler callbacks run
inline, one after another, so an unreachable ping domain does
not just delay one job — it shifts everything scheduled
behind it.
Every signal is sent with
connectTimeout(2) and
timeout(5) instead, and a failure
is swallowed: the signal either goes out fast or is quietly
dropped. A dropped signal is still reported through your
exception handler, but at most once per
schedule:run — a dead domain
would otherwise write the same trace for every signal of
every job.
Monitoring must not break the thing it monitors. That is the whole rule the package is built on.
Caveats worth knowing before you wire it up
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.
The macros are runtime macros. They are attached to
Illuminate\Console\Scheduling\Event
on boot, so your IDE will flag them as "method not found".
The package ships _ide_helper.php
with the signatures — point your IDE at it once.
Where heartbeats stop
A green check proves the job ran — not that its outcome is real. A certificate renewal that ran but whose reload 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.
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 actually on the wire. Heartbeats watch the work, HTTP checks watch the result — both on the same dashboard, with the same alert channels.
Questions
Why is onFailure() not enough?
onFailure fires only when the task runs and fails. It cannot report the task that never started: a crontab wiped by a server migration, a supervisor that stopped restarting, a deploy that dropped the schedule changes. The most dangerous failure is the one where your code never executes, and an in-process hook cannot see it by definition.
How is this different from thenPing()?
thenPing is the same dead man's switch, wired by hand: you create a check in a dashboard, copy its URL into the code, and repeat per task. The schedule then lives in two places and drifts. pingCronaliveSlug creates the check on the first ping and reads the schedule off the scheduled task itself, so there is nothing to retype and nothing to desynchronise.
Can a ping slow down or break my scheduler?
No. Every signal is sent with a 2-second connect timeout and a 5-second total timeout, and a failure is swallowed: the signal either goes out fast or is quietly dropped. The macros deliberately avoid Laravel's pingBefore/thenPing callbacks, which wait on the scheduler's default client — up to 90 seconds for a job with three signals, inline, delaying everything scheduled behind it.
What happens if I change a task's schedule in code?
Nothing, on purpose. Schedule parameters apply only when the check is created; after that pings just count, so a deploy can never rewrite a schedule someone tuned in the dashboard. Update the check in the dashboard or via the API, or delete it and let the next ping recreate it.
Which Laravel versions are supported?
Laravel 10, 11, 12 and 13, on PHP 8.1+. Every major is covered by the same integration suite in CI — scheduler macros, signal timeouts and the swallowing of a dead ping domain are asserted against a real framework of each version, not against mocks.
What does it cost?
The free plan covers ten checks with no card, which is enough to wire up the jobs you actually worry about. The SDK itself is MIT-licensed and open.
Not using the scheduler for everything?
The same check works with a plain crontab line — see cron job monitoring — and if you just want to see when an expression actually fires, the cron expression tester is free and needs no account.
Weighing this up against something else? The comparisons with Healthchecks.io and Cronitor have the price arithmetic written out, and a section on where each of them beats us.
Wire up the jobs you actually worry about
Ten checks on the free plan, no card. One
composer require and one method
call per task — the checks appear by themselves on the next
scheduler tick.