Navigation

Laravel

Laravel Health Checks: Monitor App State in 2025

Learn how to implement Laravel health checks to monitor your application state. Boost uptime with automated monitoring and alerting systems.

Table Of Contents

Introduction

Every Laravel developer has experienced that sinking feeling when users report that "the website is down" while your server monitoring shows everything is green. The truth is, a running server doesn't guarantee a healthy application. Your database might be unreachable, your cache could be failing, or your queue workers might have stopped processing jobs—all while your web server continues to serve error pages.

Laravel health checks solve this critical gap by providing automated ways to monitor your application's internal state and dependencies. Rather than waiting for users to report issues, health checks proactively monitor essential services and alert you to problems before they impact your users.

In this comprehensive guide, you'll learn how to implement robust health checks in your Laravel application, set up automated monitoring, configure alerting systems, and establish best practices that will keep your application running smoothly. Whether you're managing a small startup application or an enterprise-level system, these techniques will help you maintain higher uptime and catch issues early.

What Are Laravel Health Checks?

Laravel health checks are automated tests that verify whether your application and its dependencies are functioning correctly. Unlike traditional server monitoring that only checks if your web server is responding, health checks examine the actual state of your application's critical components.

Core Components Monitored

A comprehensive health check system typically monitors:

  • Database connectivity and query performance
  • Cache systems (Redis, Memcached) availability
  • Queue workers and job processing status
  • File system permissions and storage availability
  • External APIs and third-party service connections
  • Application-specific business logic requirements

Benefits of Implementation

Implementing health checks provides several key advantages:

  • Proactive issue detection before users are affected
  • Faster troubleshooting with specific component status
  • Improved uptime through early warning systems
  • Better incident response with detailed health reports
  • Enhanced monitoring integration with tools like Pingdom or New Relic

Setting Up Laravel Health Checks

The most popular and robust solution for Laravel health checks is the spatie/laravel-health package, which provides a comprehensive framework for monitoring your application state.

Installing the Health Package

Begin by installing the package via Composer:

composer require spatie/laravel-health

After installation, publish and run the migrations:

php artisan vendor:publish --tag="health-migrations"
php artisan migrate

Publish the configuration file to customize your health checks:

php artisan vendor:publish --tag="health-config"

Basic Configuration

The health check configuration is stored in config/health.php. Here's a basic setup:

<?php

return [
    'oh_dear_endpoint' => [
        'enabled' => env('OH_DEAR_HEALTH_CHECK_ENABLED', true),
        'secret' => env('OH_DEAR_HEALTH_CHECK_SECRET'),
        'url' => '/health',
    ],

    'checks' => [
        Spatie\Health\Checks\Checks\OptimizedAppCheck::class,
        Spatie\Health\Checks\Checks\DebugModeCheck::class,
        Spatie\Health\Checks\Checks\EnvironmentCheck::class,
        Spatie\Health\Checks\Checks\DatabaseCheck::class,
        Spatie\Health\Checks\Checks\CacheCheck::class,
        Spatie\Health\Checks\Checks\QueueCheck::class,
        Spatie\Health\Checks\Checks\ScheduleCheck::class,
    ],
];

Creating Your First Health Check Endpoint

Add the health check route to your routes file:

// routes/web.php
Route::get('/health', [\Spatie\Health\Http\Controllers\HealthCheckController::class, 'check']);

Now you can access your health check endpoint at https://yourapp.com/health and receive a JSON response showing the status of all configured checks.

Essential Health Check Types

Database Health Checks

Database connectivity is typically the most critical component to monitor. The package includes several database-related checks:

// Custom database performance check
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;

class DatabasePerformanceCheck extends Check
{
    public function run(): Result
    {
        $start = microtime(true);
        
        try {
            DB::select('SELECT 1');
            $duration = (microtime(true) - $start) * 1000;
            
            if ($duration > 1000) { // More than 1 second
                return Result::make()->failed("Database query took {$duration}ms");
            }
            
            return Result::make()->ok();
        } catch (Exception $e) {
            return Result::make()->failed("Database connection failed: {$e->getMessage()}");
        }
    }
}

Cache System Monitoring

Cache failures can severely impact application performance. Monitor your cache systems with dedicated checks:

use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
use Illuminate\Support\Facades\Cache;

class CachePerformanceCheck extends Check
{
    public function run(): Result
    {
        $testKey = 'health_check_' . time();
        $testValue = 'test_data';
        
        try {
            // Test write operation
            Cache::put($testKey, $testValue, 60);
            
            // Test read operation
            $retrieved = Cache::get($testKey);
            
            // Cleanup
            Cache::forget($testKey);
            
            if ($retrieved !== $testValue) {
                return Result::make()->failed('Cache read/write mismatch');
            }
            
            return Result::make()->ok();
        } catch (Exception $e) {
            return Result::make()->failed("Cache error: {$e->getMessage()}");
        }
    }
}

Queue Worker Health Checks

Queue workers are essential for background job processing. Monitor their status to ensure jobs are being processed:

use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
use Illuminate\Support\Facades\DB;

class QueueWorkerCheck extends Check
{
    public function run(): Result
    {
        // Check for failed jobs
        $failedJobs = DB::table('failed_jobs')->count();
        
        if ($failedJobs > 10) {
            return Result::make()->failed("Too many failed jobs: {$failedJobs}");
        }
        
        // Check for old pending jobs
        $oldJobs = DB::table('jobs')
            ->where('created_at', '<', now()->subMinutes(30))
            ->count();
            
        if ($oldJobs > 0) {
            return Result::make()->failed("Jobs not processing: {$oldJobs} jobs older than 30 minutes");
        }
        
        return Result::make()->ok();
    }
}

Advanced Health Check Implementations

Custom Application-Specific Checks

Beyond infrastructure monitoring, create checks for your specific business logic:

use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;

class PaymentGatewayCheck extends Check
{
    public function run(): Result
    {
        try {
            // Test payment gateway connectivity
            $response = Http::timeout(10)->get('https://api.paymentgateway.com/health');
            
            if ($response->successful()) {
                return Result::make()->ok('Payment gateway responsive');
            }
            
            return Result::make()->failed('Payment gateway unreachable');
        } catch (Exception $e) {
            return Result::make()->failed("Payment gateway error: {$e->getMessage()}");
        }
    }
}

File System and Storage Checks

Monitor your application's ability to read and write files:

class StorageHealthCheck extends Check
{
    public function run(): Result
    {
        $testFile = 'health_check_' . time() . '.txt';
        $testContent = 'Health check test';
        
        try {
            // Test write
            Storage::disk('local')->put($testFile, $testContent);
            
            // Test read
            $content = Storage::disk('local')->get($testFile);
            
            // Cleanup
            Storage::disk('local')->delete($testFile);
            
            if ($content !== $testContent) {
                return Result::make()->failed('File system read/write mismatch');
            }
            
            // Check available space
            $freeSpace = disk_free_space(storage_path());
            $freeSpaceGB = $freeSpace / (1024 * 1024 * 1024);
            
            if ($freeSpaceGB < 1) {
                return Result::make()->failed("Low disk space: {$freeSpaceGB}GB remaining");
            }
            
            return Result::make()->ok();
        } catch (Exception $e) {
            return Result::make()->failed("Storage error: {$e->getMessage()}");
        }
    }
}

Performance-Based Health Checks

Monitor application performance metrics:

class PerformanceHealthCheck extends Check
{
    public function run(): Result
    {
        $start = microtime(true);
        
        // Simulate typical application load
        User::take(10)->get();
        $orders = Order::with('items')->take(5)->get();
        
        $duration = (microtime(true) - $start) * 1000;
        
        if ($duration > 2000) { // More than 2 seconds
            return Result::make()->failed("Application response slow: {$duration}ms");
        }
        
        if ($duration > 1000) { // Warning threshold
            return Result::make()->warning("Application response degraded: {$duration}ms");
        }
        
        return Result::make()->ok("Response time: {$duration}ms");
    }
}

Monitoring and Alerting Integration

Setting Up Automated Monitoring

Configure automated monitoring by running health checks on a schedule. Add this to your app/Console/Kernel.php:

protected function schedule(Schedule $schedule)
{
    $schedule->command('model:prune', ['--model' => [HealthCheckResultHistory::class]])
        ->daily();
        
    $schedule->call(function () {
        Artisan::call('health:check');
    })->everyMinute();
}

Integration with External Services

Oh Dear Integration

Oh Dear provides seamless integration with Laravel health checks:

// In your .env file
OH_DEAR_HEALTH_CHECK_ENABLED=true
OH_DEAR_HEALTH_CHECK_SECRET=your-secret-key

// The package automatically handles Oh Dear integration

Custom Webhook Notifications

Create custom webhooks for your monitoring tools:

use Spatie\Health\Facades\Health;

class HealthCheckNotification
{
    public function handle()
    {
        $results = Health::check();
        
        if ($results->containsFailingCheck()) {
            $this->sendSlackNotification($results);
            $this->sendEmailAlert($results);
        }
    }
    
    private function sendSlackNotification($results)
    {
        $failedChecks = $results->failingChecks();
        
        $message = "🚨 Health Check Alert\n";
        foreach ($failedChecks as $check) {
            $message .= "• {$check->label}: {$check->notificationMessage}\n";
        }
        
        Http::post(config('services.slack.webhook'), [
            'text' => $message
        ]);
    }
}

Dashboard and Reporting

Create a simple health dashboard for your team:

// routes/web.php
Route::get('/admin/health', function () {
    $results = Health::check();
    return view('admin.health', compact('results'));
});
{{-- resources/views/admin/health.blade.php --}}
@extends('layouts.app')

@section('content')
<div class="container">
    <h1>Application Health Dashboard</h1>
    
    <div class="row">
        @foreach($results->storedCheckResults as $result)
            <div class="col-md-4 mb-3">
                <div class="card @if($result->status === 'failed') border-danger @elseif($result->status === 'warning') border-warning @else border-success @endif">
                    <div class="card-body">
                        <h5 class="card-title">{{ $result->check_name }}</h5>
                        <p class="card-text">
                            <span class="badge @if($result->status === 'failed') bg-danger @elseif($result->status === 'warning') bg-warning @else bg-success @endif">
                                {{ ucfirst($result->status) }}
                            </span>
                        </p>
                        @if($result->notification_message)
                            <p class="text-muted">{{ $result->notification_message }}</p>
                        @endif
                        <small class="text-muted">Last checked: {{ $result->updated_at->diffForHumans() }}</small>
                    </div>
                </div>
            </div>
        @endforeach
    </div>
</div>
@endsection

Troubleshooting Common Issues

Health Check Performance Problems

If health checks are taking too long, consider these optimizations:

1. Implement Timeouts

class OptimizedDatabaseCheck extends Check
{
    public function run(): Result
    {
        try {
            DB::select(DB::raw('SELECT 1'), [], 5); // 5-second timeout
            return Result::make()->ok();
        } catch (QueryException $e) {
            if (str_contains($e->getMessage(), 'timeout')) {
                return Result::make()->failed('Database timeout');
            }
            return Result::make()->failed("Database error: {$e->getMessage()}");
        }
    }
}

2. Use Connection Pooling

// config/database.php
'connections' => [
    'health_check' => [
        'driver' => 'mysql',
        'host' => env('DB_HOST', '127.0.0.1'),
        'database' => env('DB_DATABASE', 'forge'),
        // Dedicated connection for health checks
        'options' => [
            PDO::ATTR_TIMEOUT => 5,
        ],
    ],
],

False Positive Alerts

Reduce false positives with smart retry logic:

class ReliableCheck extends Check
{
    private $retries = 3;
    
    public function run(): Result
    {
        $lastException = null;
        
        for ($i = 0; $i < $this->retries; $i++) {
            try {
                // Your check logic here
                return Result::make()->ok();
            } catch (Exception $e) {
                $lastException = $e;
                if ($i < $this->retries - 1) {
                    sleep(1); // Wait 1 second before retry
                }
            }
        }
        
        return Result::make()->failed("Failed after {$this->retries} attempts: {$lastException->getMessage()}");
    }
}

Memory and Resource Management

Prevent health checks from consuming too many resources:

class ResourceAwareCheck extends Check
{
    public function run(): Result
    {
        $memoryLimit = ini_get('memory_limit');
        $currentMemory = memory_get_usage(true);
        
        // Don't run if memory usage is too high
        if ($currentMemory > (0.8 * $this->parseMemoryLimit($memoryLimit))) {
            return Result::make()->skipped('Skipped due to high memory usage');
        }
        
        // Your check logic here
        return Result::make()->ok();
    }
    
    private function parseMemoryLimit($limit)
    {
        $unit = strtolower(substr($limit, -1));
        $value = (int) substr($limit, 0, -1);
        
        switch ($unit) {
            case 'g': return $value * 1024 * 1024 * 1024;
            case 'm': return $value * 1024 * 1024;
            case 'k': return $value * 1024;
            default: return $value;
        }
    }
}

Best Practices and Security

Security Considerations

1. Protect Health Check Endpoints

// routes/web.php
Route::middleware(['auth', 'can:view-health-checks'])->group(function () {
    Route::get('/admin/health', [HealthController::class, 'dashboard']);
});

// For external monitoring, use secret tokens
Route::get('/health', function (Request $request) {
    if ($request->get('token') !== config('app.health_check_token')) {
        abort(403);
    }
    
    return app(HealthCheckController::class)->check();
});

2. Limit Information Exposure

class SecureHealthCheck extends Check
{
    public function run(): Result
    {
        try {
            // Perform your check
            return Result::make()->ok('Service operational');
        } catch (Exception $e) {
            // Don't expose sensitive error details
            Log::error('Health check failed: ' . $e->getMessage());
            return Result::make()->failed('Service unavailable');
        }
    }
}

Performance Optimization

1. Cache Health Check Results

use Illuminate\Support\Facades\Cache;

class CachedHealthCheck extends Check
{
    private $cacheKey = 'health_check_result';
    private $cacheTTL = 60; // seconds
    
    public function run(): Result
    {
        return Cache::remember($this->cacheKey, $this->cacheTTL, function () {
            return $this->performActualCheck();
        });
    }
    
    private function performActualCheck(): Result
    {
        // Your actual health check logic
        return Result::make()->ok();
    }
}

2. Implement Circuit Breaker Pattern

class CircuitBreakerCheck extends Check
{
    private $failureThreshold = 5;
    private $recoveryTime = 300; // 5 minutes
    
    public function run(): Result
    {
        $failureCount = Cache::get('health_check_failures', 0);
        $lastFailure = Cache::get('health_check_last_failure');
        
        // Circuit is open
        if ($failureCount >= $this->failureThreshold) {
            if ($lastFailure && (time() - $lastFailure) < $this->recoveryTime) {
                return Result::make()->skipped('Circuit breaker open');
            }
            // Reset circuit breaker
            Cache::forget('health_check_failures');
            Cache::forget('health_check_last_failure');
        }
        
        try {
            // Perform check
            $result = $this->performCheck();
            
            if ($result->failed()) {
                Cache::increment('health_check_failures');
                Cache::put('health_check_last_failure', time());
            } else {
                Cache::forget('health_check_failures');
            }
            
            return $result;
        } catch (Exception $e) {
            Cache::increment('health_check_failures');
            Cache::put('health_check_last_failure', time());
            return Result::make()->failed($e->getMessage());
        }
    }
}

Integration with CI/CD and DevOps

Docker Health Checks

Integrate Laravel health checks with Docker health checks:

# Dockerfile
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost/health || exit 1

Kubernetes Probes

Configure Kubernetes to use your Laravel health checks:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: laravel-app
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5

CI/CD Pipeline Integration

Include health checks in your deployment pipeline:

# .github/workflows/deploy.yml
- name: Wait for deployment health check
  run: |
    timeout 300 bash -c 'until curl -f ${{ secrets.APP_URL }}/health; do sleep 10; done'
    
- name: Verify all health checks pass
  run: |
    response=$(curl -s ${{ secrets.APP_URL }}/health)
    if echo "$response" | grep -q '"failed"'; then
      echo "Health checks failed!"
      exit 1
    fi

FAQ

Q: How often should I run health checks? A: For critical systems, run basic health checks every 1-2 minutes. More resource-intensive checks can run every 5-10 minutes. Adjust frequency based on your application's criticality and the check's resource requirements.

Q: What's the difference between health checks and application monitoring? A: Health checks verify that your application components are functioning correctly, while monitoring tracks performance metrics over time. Health checks are binary (working/not working), whereas monitoring provides trends and analytics.

Q: Should health checks access production databases? A: Yes, but use lightweight queries and consider read replicas for database checks. Simple queries like SELECT 1 are ideal as they test connectivity without impacting performance.

Q: How do I handle health check failures during deployments? A: Implement a grace period during deployments and use separate readiness and liveness checks. Configure your load balancer to remove instances that fail readiness checks while allowing time for deployment completion.

Q: Can health checks impact application performance? A: When implemented properly with caching, timeouts, and resource limits, health checks should have minimal impact. Avoid running expensive operations and use dedicated database connections for health checks.

Q: What should I do if external service health checks fail? A: Implement graceful degradation in your application. Log the failures, alert your team, but don't let external service failures bring down your entire application. Consider circuit breaker patterns for resilience.

Conclusion

Laravel health checks are essential for maintaining robust, reliable applications in production environments. By implementing comprehensive monitoring of your database, cache, queues, and application-specific components, you can detect issues before they impact your users and respond quickly when problems arise.

The key takeaways from this guide include: establishing both infrastructure and application-level health checks, implementing proper security and performance optimization, integrating with your existing DevOps workflows, configuring appropriate alerting and notification systems, and following best practices for resource management and error handling.

Remember that health checks are just one part of a comprehensive monitoring strategy. Combine them with application performance monitoring, log aggregation, and business metrics tracking for complete visibility into your application's health.

Ready to implement health checks in your Laravel application? Start with the basic database and cache checks outlined in this guide, then gradually add more sophisticated monitoring as your needs grow. Share your experience with Laravel health checks in the comments below—what challenges have you faced, and what creative solutions have you implemented?

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Laravel