jpvdw86/symfony-health-check-bundle
A Symfony bundle that adds a health check endpoint to your application, aggregating checks such as database, cache, and disk to report overall status for monitoring tools, load balancers, and uptime probes.
This is a Symfony bundle, not a Laravel package. There is no extra.laravel.providers, no service provider, and no auto-discovery. composer require jpvdw86/symfony-health-check-bundle will install code into vendor/, but in a stock Laravel app it will do nothing on its own. Treat it as one of two things:
/health and probes the same infrastructure your Laravel app uses (DB, Redis, queue, storage).Minimal first steps (Symfony context, to understand the package):
composer require jpvdw86/symfony-health-check-bundle
// config/bundles.php
return [
// ...
Vendor\HealthCheckBundle\HealthCheckBundle::class => ['all' => true],
];
# config/routes.yaml
health_check:
resource: "@HealthCheckBundle/Resources/config/routes.yaml"
Then look here first, in this order:
vendor/jpvdw86/symfony-health-check-bundle/src/ — find the bundle class, the controller, and the checker interface/contract.DependencyInjection/Configuration.php — this is the source of truth for what is configurable. If it isn’t in the config tree, it isn’t a supported option.DependencyInjection/*Extension.php — shows which services are tagged and how checkers are collected.health_check.checker (or similarly named) service tag — this is the main extension point.First use case: a database connectivity check exposed at GET /health returning 200 with a JSON body when the DB answers and 503 when it doesn’t. Wire that up, curl it locally, then wire it into your probe/uptime monitor.
For a Laravel app, the realistic “first use case” is a 30-line port:
// routes/web.php or routes/api.php
Route::get('/health', function () {
try {
DB::connection()->getPdo();
return response()->json(['status' => 'ok']);
} catch (\Throwable $e) {
report($e);
return response()->json(['status' => 'fail'], 503);
}
});
Then grow it toward the package’s model: multiple named checkers aggregated into one report.
The package’s useful idea is that each dependency is an isolated checker, registered as a service, tagged, and run by an aggregator. Recreate that in Laravel rather than trying to boot the bundle:
// app/Health/Checker.php
interface Checker
{
public function name(): string;
public function check(): CheckResult; // { healthy: bool, message: ?string, meta: array }
}
// app/Providers/HealthServiceProvider.php
public function register(): void
{
$this->app->tag([
\App\Health\Checkers\DatabaseChecker::class,
\App\Health\Checkers\CacheChecker::class,
\App\Health\Checkers\QueueChecker::class,
\App\Health\Checkers\StorageChecker::class,
], 'health.checker');
$this->app->singleton(HealthManager::class, fn ($app) =>
new HealthManager($app->tagged('health.checker'))
);
}
This gives you the same benefits the bundle gives Symfony devs: add a check by adding a class + tag, no controller edits.
Do not put a DB check behind a Kubernetes liveness probe. A transient DB outage will restart every pod and turn a degraded dependency into a full outage. Mirror the Symfony convention of cheap-vs-deep endpoints:
GET /health/live — “is the PHP process alive?” No I/O. Always 200 unless the app is broken.GET /health/ready — DB, Redis, queue backlog, storage writability. Returns 503 when a dependency is down.GET /health/deep — full report, optional auth/token, used by dashboards and humans.Load balancers and orchestrators only understand the status code; humans read the body. Return both:
return response()->json([
'status' => $report->isHealthy() ? 'ok' : 'fail',
'checks' => $report->toArray(), // name => {status, duration_ms, message}
'version' => config('app.version'),
], $report->isHealthy() ? 200 : 503);
Keep the body shape stable — your uptime monitor will parse it.
Probes often fire every 5–10 seconds per replica. Hitting the DB and Redis on every probe is self-inflicted load. Cache the aggregated report briefly, keyed by a hash of the enabled checkers:
$report = Cache::remember('health:report', now()->addSeconds(10), fn () =>
$this->manager->run()
);
Add an ?fresh=1 (auth-protected) escape hatch for on-call debugging.
| Concept | Symfony bundle equivalent | Laravel implementation |
|---|---|---|
| DB | PDO/Doctrine check | DB::select('select 1') or DB::connection()->getPdo() |
| Cache | cache pool check | Cache::store()->put('health', 1, 5); Cache::store()->get('health') |
| Queue | messenger transport | Queue::size() vs threshold |
| Storage | filesystem adapter | Storage::disk()->put('.health', '1'); exists() |
| Search/HTTP | custom checker | Http::timeout(2)->get(...) |
Each checker must have an explicit timeout. A health check that can hang is worse than no health check.
Use the same endpoint as a post-deploy smoke test in your pipeline:
curl -fsS --max-time 5 https://app.example.com/health/ready || exit 1
This is where these packages earn their keep day-to-day: one contract shared by k8s, the load balancer, Uptime Kuma, and CI.
Run it only inside a Symfony application. If a Laravel app must consume it, put it behind an HTTP sidecar or a small Symfony service and have Laravel call /health/ready via Http::timeout(2). Do not attempt to register the bundle in config/app.php — Laravel has no bundle kernel.
composer require is a no-op at runtime. Check composer why jpvdw86/symfony-health-check-bundle and vendor/ contents before assuming it’s wired up.symfony/framework-bundle, symfony/http-kernel, symfony/dependency-injection, and symfony/config. Installing it into Laravel can pull a large Symfony surface area for zero runtime benefit. Check composer.lock diff.app/Health/ rather than depending on the package./health/deep by IP/token or bind it to an internal listener. Never let APP_DEBUG=true output appear in the health response.check() in try/catch, record status: fail with a short message, and log the full exception. The aggregator should always produce a report./health/deep.config/packages/*.yaml, run bin/console cache:clear. Use bin/console debug:container --tag=<checker-tag> to verify your custom checker is actually registered, and bin/console debug:router to confirm the route was imported.Configuration.php, don’t assume it exists — decorate the service instead.curl -i to see the status code (not just the body), check the app log for swallowed checker exceptions, verify the tag with debug:container, and test with the dependency deliberately stopped (e.g., docker compose stop redis) to confirm you actually get a 503.spatie/laravel-health and Laravel’s built-in php artisan about cover most of this with active maintenance and first-class Laravel integration. For most Laravel teams, that is the correct day-to-day tool; this bundle is best used as a design reference or in a genuinely Symfony codebase.How can I help you explore Laravel packages today?