pbmedia/laravel-api-health
pbmedia/laravel-api-health lets you monitor Laravel API endpoints with configurable health checks, scheduled runs, and notifications when an endpoint is down or unhealthy.
1. Install and publish
composer require pbmedia/laravel-api-health
php artisan vendor:publish --tag="api-health-config"
PHP 8.2 is supported as of 5.4.0. Laravel's package auto-discovery registers the service provider and the Health facade (Pbmedia\ApiHealth\Facades\Health). The published config/api-health.php is the first file to read — it controls notification routing, thresholds, and the cache/store used to remember previous check results.
2. Register your first check
Checks are declared in a service provider's boot() method (a dedicated HealthServiceProvider keeps things tidy):
use Pbmedia\ApiHealth\Facades\Health;
use Illuminate\Support\Facades\Http;
Health::check('stripe', function () {
return Http::timeout(5)
->get('https://api.stripe.com/v1/charges')
->successful();
});
The closure (or invokable class) returns a truthy value for "healthy". Anything falsy — or an exception — marks the check as failed and triggers the configured notifier.
3. Schedule it
The package ships an artisan command that evaluates all registered checks. Wire it into the scheduler:
// app/Console/Kernel.php (Laravel ≤10) or routes/console.php (Laravel 11+)
$schedule->command('api-health:check')->everyFiveMinutes();
4. Verify
Run the command manually first (php artisan api-health:check -v) before trusting the scheduler. That single invocation tells you whether checks resolve, whether the notifier is configured, and whether anything is silently swallowing exceptions.
First realistic use case: you depend on a third-party API (Stripe, Mailgun, an internal microservice) and want a Slack ping when it degrades, rather than discovering it from a customer ticket.
Inline closures for simple probes, classes for anything reused
A closure is fine for Http::get(...)->successful(). As soon as a check has non-trivial setup (auth tokens, payload validation, multi-step assertions), extract it into a dedicated class and register the class name. This keeps the service provider readable and makes the check unit-testable in isolation.
Assert on semantics, not just status codes
->successful() is a weak signal. Prefer checks that validate a meaningful contract:
Health::check('payments-api', function () {
$response = Http::timeout(5)->get(config('services.payments.url') . '/ping');
return $response->successful()
&& $response->json('status') === 'ok';
});
A 200 that returns a maintenance page or an error envelope is still an outage.
Group and route notifications per team/service
Register checks with names that map to ownership (billing:stripe, ops:redis) and route notifications to the channel that owns that dependency. A single firehose channel gets muted within a week; per-team channels survive.
Debounce to avoid alert storms
A flapping dependency can generate dozens of notifications an hour, and a scheduler running every minute amplifies it. Set a failure threshold in the config (or track consecutive failures yourself in cache) so a notification only fires after N consecutive failures, and send a single "recovered" message on the way back up.
Keep checks off the request path
Never call a check inside a controller or middleware. Checks do network I/O with timeouts; running them per-request will tank latency and multiply outbound traffic. The scheduler command is the only sane execution context.
Pair with an internal status endpoint
If you want a human-facing view, expose a route that reads the last known results rather than re-running checks:
Route::get('/internal/health', fn () => Health::results());
Protect it behind auth or an IP allowlist.
Test checks with Http::fake()
Because a check is just a closure returning a boolean, you can test it without booting the scheduler:
Http::fake(['api.stripe.com/*' => Http::response(['status' => 'ok'], 200)]);
$this->assertTrue((new StripeCheck)());
Fake the failure path too — an untested failure branch is a check you can't trust.
Integration with existing observability
If you already run Sentry/Horizon/Oh Dear, resist duplicating. Use this package for the narrow slice it does well (third-party dependency probes with notifications) and let your APM handle latency and error-rate metrics.
Maintenance status is the single biggest consideration. 5.4.0 adds PHP 8.2 support, but that is a runtime-compatibility patch, not a signal of active development: no Laravel-version work accompanies it, and the package has effectively sat still since 2022. Framework compatibility for Laravel 10/11+ is still unverified and, if broken, won't be fixed upstream. Before adopting:
composer require pbmedia/laravel-api-health --dry-run
Check whether its illuminate/* constraints resolve against your framework version. If they don't, you'll need --with-all-dependencies and possibly a replace/fork — at which point evaluate whether maintaining a fork is worth it versus spatie/laravel-health or Laravel's built-in health route. PHP 8.2 support means the PHP side of the install won't block you; the Laravel side still might.
Zero dependents on Packagist plus a single compatibility-only release is consistent with a dormant package, not a hidden gem. Budget for the possibility that you are the maintainer now.
Explicit HTTP timeouts are mandatory. Guzzle's default is effectively unbounded relative to a scheduler tick. A hung check blocks the whole run and every subsequent check in the queue. Always ->timeout(5)->connectTimeout(2).
Exceptions escape silently unless you catch them. A check that throws (DNS failure, SSL error, JSON parse error on a non-JSON body) is a failure, but how the package surfaces it depends on its internals — some versions log and continue, some abort the run. Wrap risky checks in try/catch and return false explicitly so behaviour is deterministic.
Cache driver matters. State tracking (previous status, consecutive failure counts) lives in the cache. If CACHE_DRIVER=array or your cache is flushed regularly, debounce logic and "recovered" notifications behave unpredictably. Use a persistent store (redis, database) in production.
Config caching will bite you. After editing config/api-health.php, run php artisan config:cache — or forget it and wonder why your notification routing change did nothing. Conversely, during debugging, php artisan config:clear.
Don't run checks with withoutOverlapping() omitted. If a check hangs, the next scheduled tick starts a second instance. Add ->withoutOverlapping() to the scheduler entry.
Notification failures are silent. If your Slack webhook is misconfigured, the check runs, fails, and nothing happens — the monitoring tool itself becomes a blind spot. Send a deliberate test notification on deploy and assert it arrives.
Extension points are its best feature. The closure/class-based check design means you can monitor literally anything: queue depth, disk space, a database replica lag figure — not just HTTP endpoints. If you keep the package, lean on this rather than its defaults.
Migration note. If you're evaluating alternatives: spatie/laravel-health is actively maintained, has a comparable check-based model, and supports current Laravel releases. Laravel 11's built-in health route (via php artisan about and the Health endpoint) covers basic liveness. Given the dormant upstream, plan for one of these as the eventual destination.
How can I help you explore Laravel packages today?