Running checks
When schedule.enabled is true (the default), the package registers alerts:perform-health-checks on Laravel’s scheduler with withoutOverlapping(), at the schedule.frequency method. Whenever history is enabled it also schedules alerts:prune-runs daily, whatever schedule.enabled says. To wire the perform command yourself, disable that flag and add it to routes/console.php — the daily prune stays scheduled:
use Illuminate\Support\Facades\Schedule;
Schedule::command('alerts:perform-health-checks')->everyMinute();php artisan alerts:perform-health-checksThe command calls Health::runDue(), which walks every scheduled row, dispatches the configured job (HealthCheckJob by default) for each row whose cron is due at the current minute and returns how many it queued — the command prints that count. Soft-deleted and on-demand rows are skipped. A row whose cron cannot be evaluated (written by a seeder or by hand, since save() validates) is skipped and reported to your exception handler, and the rows after it are still dispatched. Call it yourself from anywhere:
use RoundlyConsulting\Alerts\Facades\Health;
$queued = Health::runDue(); // int — what alerts:perform-health-checks callsBecause due-ness is checked at the minute the command runs, keep it at everyMinute unless all your crons line up with a coarser cadence.
Scheduler and queue
HealthCheckJob is a queued job, so both Laravel’s scheduler and a queue worker must be running. With the sync queue driver, checks run inline.
# Laravel's scheduler must run for scheduled checks to fire
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
# Locally: php artisan schedule:work
# And a queue worker to execute the queued HealthCheckJob:
php artisan queue:workWhat one run does
Every run — queued or synchronous — goes through one internal pipeline, RunHealthCheckAction. Options are read from the persisted row, so they survive queue serialization:
- Run check() under the row’s timeout and an exception guard, timing it.
- Record a HealthCheckRun (when history is enabled).
- A skipped result stops here — no counters, no alert side effects.
- Update consecutive_failures or consecutive_successes. Increments are atomic, so overlapping runs never lose a count.
- Check for an active mute on the check key, its tags or *.
- On failure: an open alert first follows the result — its status, message and muted flag. Once failAfter() is reached, open an alert (or reuse the open one), dispatch HealthCheckFailed, then notify or escalate. A check has at most one open alert, even when two runs overlap.
- On success: once recoverAfter() is reached, close the open alert, reset its escalation level and dispatch HealthCheckRecovered — once, even when two runs overlap.
Run a check now
Run a check synchronously against an owner. It performs the same alert, notify and recover side effects as the queued job and returns the CheckResult. A class name runs the instance you registered for that class, with its configuration; an instance runs that instance. Neither changes the registered check:
use RoundlyConsulting\Alerts\Facades\Health;
$result = Health::for($team)->run(DiskUsageCheck::class);
$result->status; // Status::Ok | Warning | Failed | Skipped
// Run one of the team's scheduled rows as-is (another owner's row is refused):
Health::for($team)->run($team->healthChecks()->first());
// An inline check has no class — run it by its registered key:
Health::for($team)->run('redis-up');Running a check now never starts monitoring it. When the owner already has a scheduled row for the check, the run goes through that row and shares its counters, history and alerts. Otherwise the package seeds an on-demand row (frequency is null) that keeps the owner’s counters, history and alerts for that check between manual runs. The scheduler never queues it and monitors() does not list it — to monitor a check on a schedule, use monitor().
Given one of the owner’s rows, run() runs that row as-is; a row that belongs to another owner throws InvalidHealthCheck. A class registered under several keys with as() is ambiguous by class name — run it by key.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.