paulund
#laravel #observability #devops

Health Checks

A health check endpoint lets your load balancer, container orchestrator, or uptime monitor confirm that your application is alive and its dependencies are reachable. Without one, a deployment that starts the web server but fails to connect to the database will silently serve errors until a user reports them.

A Simple Health Route

The minimum viable health check is a route that returns HTTP 200 when the app is running:

Route::get('/health', fn () => response()->json(['status' => 'ok']));

This is enough for a basic uptime monitor. For anything beyond that, you want to verify your critical dependencies too.

Checking Dependencies

A more useful health check tests whether the application can actually do its job:

Route::get('/health', function () {
    $checks = [];

    // Database
    try {
        DB::connection()->getPdo();
        $checks['database'] = 'ok';
    } catch (\Exception) {
        $checks['database'] = 'fail';
    }

    // Cache
    try {
        Cache::put('health', true, 5);
        $checks['cache'] = Cache::get('health') === true ? 'ok' : 'fail';
    } catch (\Exception) {
        $checks['cache'] = 'fail';
    }

    // Queue (check that the failed_jobs table is reachable)
    try {
        DB::table('failed_jobs')->count();
        $checks['queue'] = 'ok';
    } catch (\Exception) {
        $checks['queue'] = 'fail';
    }

    $healthy = !in_array('fail', $checks);

    return response()->json([
        'status' => $healthy ? 'ok' : 'degraded',
        'checks' => $checks,
    ], $healthy ? 200 : 503);
});

Return a 503 status code when any critical dependency is down. Load balancers use this to stop routing traffic to the unhealthy instance.

Using spatie/laravel-health

For production applications, the spatie/laravel-health package provides a structured approach with built-in checks for databases, Redis, queues, disk space, and more:

composer require spatie/laravel-health
php artisan health:publish

Register your checks in a service provider:

use Spatie\Health\Facades\Health;
use Spatie\Health\Checks\Checks\DatabaseCheck;
use Spatie\Health\Checks\Checks\RedisCheck;
use Spatie\Health\Checks\Checks\QueueCheck;
use Spatie\Health\Checks\Checks\UsedDiskSpaceCheck;

public function boot(): void
{
    Health::checks([
        DatabaseCheck::new(),
        RedisCheck::new(),
        QueueCheck::new(),
        UsedDiskSpaceCheck::new()->warnWhenUsedSpaceIsAbovePercentage(70),
    ]);
}

The package exposes a /health route automatically and can store results so you do not hammer your dependencies on every request.

Laravel 11+ Built-in Health Route

Laravel 11 ships with a built-in /up route that returns a 200 response when the application starts. You can find it in bootstrap/app.php. This is suitable for simple liveness probes but does not check dependencies.

Protecting the Endpoint

Health check endpoints should not be publicly accessible in detail. Consider two approaches:

Return minimal information publicly, detail only internally:

Route::get('/health', function () {
    // Full detail only for requests from internal networks
    if (!request()->is('192.168.*')) {
        return response()->json(['status' => 'ok']);
    }

    // ... detailed checks
});

Use middleware to restrict access:

Route::get('/health', HealthController::class)
    ->middleware('throttle:health');

Tips

  • Keep the health endpoint fast. Checks that take longer than a second will cause load balancer timeouts.
  • Do not run migrations or heavy operations in a health check.
  • Use a separate route file or exclude the health route from CSRF protection and authentication middleware.
  • Pair the health endpoint with an external uptime monitor (UptimeRobot, Better Uptime) that polls it every minute.