Framework Guide

Laravel Health Check

Set up health check endpoints and uptime monitoring for your Laravel application. Simple patterns for production-ready monitoring.

Why Monitor Your Laravel App?

Laravel applications depend on a rich ecosystem of services -- databases, Redis, queue workers, scheduled tasks, and file storage. Each can fail independently while your PHP-FPM process keeps accepting requests. External monitoring ensures you catch these partial failures before they cascade into user-facing errors.

  • Queue worker failures -- Horizon or queue workers can crash without affecting web requests, causing jobs to silently pile up
  • Scheduler not running -- If your server's cron stops, scheduled tasks (reports, cleanups, notifications) silently stop executing
  • Cache/session backend outages -- Redis going down breaks sessions, caching, and often the queue, causing cascading failures
  • Storage driver issues -- S3 or local disk failures make file uploads and media serving fail while the app itself stays up

Simple Health Route

// routes/web.php or routes/api.php
Route::get('/health', function () {
    return response()->json([
        'status' => 'healthy',
        'timestamp' => now()->toISOString()
    ]);
});

Health Controller

// app/Http/Controllers/HealthController.php
namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;

class HealthController extends Controller
{
    public function health(): JsonResponse
    {
        return response()->json(['status' => 'healthy']);
    }

    public function ready(): JsonResponse
    {
        $checks = [];
        $healthy = true;

        // Check database
        try {
            $start = microtime(true);
            DB::select('SELECT 1');
            $checks['database'] = [
                'status' => 'ok',
                'response_time_ms' => round((microtime(true) - $start) * 1000, 2)
            ];
        } catch (\Exception $e) {
            $healthy = false;
            $checks['database'] = [
                'status' => 'error',
                'message' => $e->getMessage()
            ];
        }

        // Check cache/Redis
        try {
            $start = microtime(true);
            Cache::put('health_check', 'ok', 10);
            Cache::get('health_check');
            $checks['cache'] = [
                'status' => 'ok',
                'response_time_ms' => round((microtime(true) - $start) * 1000, 2)
            ];
        } catch (\Exception $e) {
            $healthy = false;
            $checks['cache'] = [
                'status' => 'error',
                'message' => $e->getMessage()
            ];
        }

        return response()->json([
            'status' => $healthy ? 'healthy' : 'unhealthy',
            'timestamp' => now()->toISOString(),
            'checks' => $checks,
            'version' => config('app.version', 'unknown')
        ], $healthy ? 200 : 503);
    }
}

// routes/web.php
Route::get('/health', [HealthController::class, 'health']);
Route::get('/ready', [HealthController::class, 'ready']);

Using spatie/laravel-health

For a more robust solution, use the Spatie health package:

# Install
composer require spatie/laravel-health
php artisan vendor:publish --tag="health-config"

# config/health.php
use Spatie\Health\Checks\Checks\DatabaseCheck;
use Spatie\Health\Checks\Checks\CacheCheck;
use Spatie\Health\Checks\Checks\RedisCheck;
use Spatie\Health\Checks\Checks\UsedDiskSpaceCheck;

return [
    'checks' => [
        DatabaseCheck::new(),
        CacheCheck::new(),
        RedisCheck::new(),
        UsedDiskSpaceCheck::new()
            ->warnWhenUsedSpaceIsAbovePercentage(70)
            ->failWhenUsedSpaceIsAbovePercentage(90),
    ],
];

# routes/web.php
use Spatie\Health\Http\Controllers\HealthCheckResultsController;

Route::get('/health', HealthCheckResultsController::class);

Exclude from CSRF

// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
    'health',
    'ready',
];

Check Queue Health

// Check if queue workers are running
try {
    $queueSize = Queue::size('default');
    $checks['queue'] = [
        'status' => $queueSize < 1000 ? 'ok' : 'warning',
        'pending_jobs' => $queueSize
    ];
} catch (\Exception $e) {
    $checks['queue'] = [
        'status' => 'error',
        'message' => $e->getMessage()
    ];
}

Best Practices

  • Skip middleware — Health checks shouldn't require auth or CSRF
  • Check queue workers — Verify background jobs are processing
  • Monitor disk space — Laravel logs can fill up disks
  • Check scheduled tasks — Ensure cron is running

Common Laravel Production Issues

Config Cache Stale After Deploy

Running php artisan config:cache in production improves performance, but forgetting to re-cache after a deploy means your app runs with old configuration. Include a version check in your health endpoint to confirm the running config matches the deployed code.

PHP-FPM Worker Exhaustion

Long-running requests (file uploads, slow external API calls) can exhaust all PHP-FPM workers, making the app unresponsive. Health checks detect this since they also need a worker to respond. Tune pm.max_children and set request_terminate_timeout to kill stuck workers.

Log File Disk Full

Laravel writes to storage/logs/laravel.log by default. Without log rotation, this file can fill the disk, causing write failures across the entire application. Include a disk space check in your health endpoint and configure the daily log channel.

Queue Jobs Failing Silently

Failed queue jobs go to the failed_jobs table but don't trigger alerts by default. Monitor queue sizes in your health endpoint -- a growing queue suggests workers are down or overwhelmed. Use Horizon for Redis-based queues or check Queue::size() directly.

Frequently Asked Questions

How do I add a health check endpoint to Laravel?
Add a simple route in routes/web.php or routes/api.php that returns a JSON response with status 200. For comprehensive checks, create a HealthController that tests database connectivity with DB::select('SELECT 1'), cache with Cache::put/get, and queue status. Return 503 if any dependency fails.
Should I use spatie/laravel-health or write custom health checks?
For simple apps, a custom route or controller is sufficient. Use spatie/laravel-health when you need built-in checks for database, cache, Redis, disk space, queue workers, and scheduled tasks. It also stores check results and provides a dashboard. It's the most popular health check package in the Laravel ecosystem.
How do I exclude health routes from Laravel CSRF protection?
Add your health check paths to the $except array in VerifyCsrfToken.php. Alternatively, define your health routes in routes/api.php which doesn't apply CSRF middleware by default. This ensures monitoring services like UptimeSignal can access your health endpoint without a CSRF token.
How do I monitor Laravel queue workers?
Check queue health by calling Queue::size('default') to see pending job count. A growing queue indicates workers are falling behind. Also verify the queue connection (Redis/database) is reachable. For comprehensive monitoring, use spatie/laravel-health's QueueCheck which tests that a dispatched job actually gets processed.
How often should I check my Laravel application's health?
For production Laravel apps, check every 1-5 minutes. UptimeSignal's free tier checks every 5 minutes, which catches most outages quickly. Pro tier supports 1-minute intervals. Laravel's scheduler runs every minute, so monitoring at 1-minute intervals also helps detect if your cron job has stopped triggering the scheduler.

Monitor your Laravel API

Add your health endpoint to UptimeSignal and get alerted when it fails. Free tier includes 25 monitors.

Start monitoring free →

No credit card required. Commercial use allowed.

More Framework Guides