renoki-co/laravel-healthchecks
Laravel Healthchecks adds configurable health-check endpoints to your Laravel app, letting you monitor database, cache, storage, queue, and custom checks through simple HTTP responses for uptime and readiness probes.
renoki-co/laravel-healthchecks wires Laravel into Healthchecks.io — a dead-man's-switch / cron monitoring service. Your app pings a URL on a schedule; if the ping doesn't arrive within the expected period + grace, Healthchecks.io alerts you. Day-to-day, this means "my scheduler silently stopped running" becomes a page instead of a discovery three weeks later.
Requirements as of 2.1.0: PHP 8.0+ (7.3/7.4 dropped), Laravel 8.x or 9.x (7.x dropped).
Install:
composer require renoki-co/laravel-healthchecks
php artisan vendor:publish --tag=healthchecks-config
Configure via .env (never edit the published config directly — it should read from env so config:cache works):
HEALTHCHECKS_ENDPOINT=https://hc-ping.com
HEALTHCHECKS_UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Create a check in the Healthchecks.io dashboard first, copy its UUID, and set the period/grace there. The package does not create checks for you.
First use case — prove the scheduler is alive. Start with a single ping at the end of a scheduled command. That one check catches the most common real failure: cron died, queue worker died, or the app is throwing during boot. Only after that works should you fan out to per-job checks.
Where to look first: config/healthchecks.php (endpoint, UUID, timeout), then the Healthchecks facade and the service provider to see what's bound. Skim the README's examples for the exact method names in your pinned version — the facade surface is small but has changed across majors.
Ping from the scheduler. The core pattern — wrap or bracket scheduled work so a success ping only fires if the work completes:
// app/Console/Kernel.php
$schedule->command('reports:generate')
->dailyAt('02:00')
->onSuccess(fn () => Healthchecks::ping())
->onFailure(fn () => Healthchecks::fail());
Fail signals are the highest-value part. A ping that never arrives and a ping that explicitly failed are different alerts in Healthchecks.io — use fail() in catch blocks and failed() job hooks so you get "it ran and broke" rather than waiting out the grace period:
public function failed(Throwable $e): void
{
Healthchecks::fail();
}
Per-job checks with explicit UUIDs. One UUID per check gives you isolated alerts and per-job history. Where the facade accepts a UUID override, pass it per call site (or read a per-job key from config) rather than relying on the single global HEALTHCHECKS_UUID:
Healthchecks::ping(config('healthchecks.checks.reports'));
Start/finish bracketing for long jobs. For anything running longer than the check's period, send a start signal at the top and a success at the bottom, and set the check's period to cover the whole run. Otherwise a legitimately long job looks dead mid-flight.
Deploy check-ins. A ping at the end of your deploy script (Envoy task, CI step, or a post-deploy artisan command) turns "deploy silently half-finished" into an alert. Pair it with an app-boot ping so a rollback that fails to boot is caught too.
Health endpoint for uptime monitoring. If the package exposes its health route, gate it behind auth or an unguessable path before shipping — it typically probes DB/cache and is not something you want public by default.
Testing. Fake the facade (Healthchecks::shouldReceive('ping')->once()) to assert the ping is wired into the right lifecycle hook, and Http::fake() when you want to assert the actual outbound URL. Don't hit hc-ping.com in tests — it pollutes your check history and makes CI depend on an external service.
The 2.1.0 upgrade is a breaking change for older apps. If you're on Laravel 7.x or PHP 7.3/7.4, 2.1.0 will not install. Pin ^2.0 if you can't move yet, and check composer.json constraints in any package that shares the dependency tree — a transitive Laravel constraint conflict is the usual failure mode, not the package itself.
Never let a ping failure break your app. hc-ping.com being slow or down must not fail a scheduled job that otherwise succeeded. Confirm your usage wraps pings where it matters, and set a short HTTP timeout in config (a few seconds). Default HTTP timeouts of 30s will silently stall the scheduler.
config:cache and env. If your config file uses env() at the top level and you add a new HEALTHCHECKS_* key without re-running php artisan config:cache, you'll get null at runtime. Clear and re-cache in deploy scripts.
The endpoint is a URL, not an API. Pings are unauthenticated GET/POST to https://hc-ping.com/<uuid> (with /fail and /start suffixes, or an exit-status body). The UUID is the secret — treat it like a credential and don't commit it.
Grace periods are configured server-side. A "flapping" alert almost always means the check's period in the dashboard is shorter than the job's real runtime variance, not a bug in the package. Fix the check, not the code.
Scheduler overlap. Without ->withoutOverlapping(), a slow job plus a retry can produce pings that look healthy while the job is actually stalled. Combine the healthcheck with overlap protection so the signal means what you think it means.
Queue workers. Ping from the job's handle()/failed(), not from dispatch(). A ping on dispatch monitors your web request, not the work — the exact failure you were trying to catch.
Extension points. The service provider binding is the seam for swapping in your own client (e.g. to add logging or a custom HTTP client); binding a decorator around the Healthchecks contract lets you no-op all pings in local/staging via an env flag rather than sprinkling if (app()->isProduction()) through the codebase.
How can I help you explore Laravel packages today?