Skip to content

Health Checks

The application uses spatie/laravel-health for monitoring system health. In production, the scheduler runs the checks every minute (RunHealthChecksCommand) and dispatches the queue heartbeat job every five minutes (DispatchQueueCheckJobsCommand). Neither is scheduled outside production, so run php artisan health:check when you need results there. The schedule heartbeat (ScheduleCheckHeartbeatCommand) runs every minute in every environment.

The following health checks are registered by default in HealthServiceProvider:

CheckDescriptionEnvironment
DatabaseVerifies database connectivityProduction only
QueueFails when the heartbeat job hasn’t been processed within 15 minutesProduction only
CacheVerifies cache system is operationalAll
ScheduleVerifies scheduled tasks have run recentlyAll
Debug ModeEnsures debug mode is disabledNon-local
Optimized AppEnsures app is optimizedNon-local
Security AdvisoriesChecks for known package vulnerabilitiesAll
RedisVerifies Redis connectivityWhen Redis driver is in use
Directory SearchVerifies Northwestern Directory Search APIAll, but skipped locally when DIRECTORY_SEARCH_API_KEY is blank

Additional checks are available for services like Redis, Horizon, Octane, and more. See the available checks documentation for a complete list. To add a check, register it in the HealthServiceProvider.

A health check endpoint is available for external monitoring systems, protected by a secret token.

Endpoint: GET /api/health

Authentication: Requires an X-Secret-Token header matching the HEALTH_SECRET_TOKEN environment variable, compared in constant time by Chassis’s RequireSecretToken middleware (RequireSecretToken::class . ':health.secret_token' in routes/api.php). Until HEALTH_SECRET_TOKEN is set, the endpoint refuses every request with 403.

Freshness: Every request runs the checks and stores the results before responding, because oh_dear_endpoint.always_send_fresh_results is true in config/health.php.

Terminal window
curl -H "X-Secret-Token: your-secret-token" https://your-app.example.com/api/health

Response:

{
"finishedAt": 1703299200,
"checkResults": [
{
"name": "database",
"label": "Database",
"notificationMessage": "Database is operational",
"shortSummary": "OK",
"status": "ok",
"meta": {}
}
]
}

HTTP Status Codes:

CodeMeaning
200Every check returned ok or skipped
503At least one check returned warning, failed or crashed
403Missing or incorrect token, or HEALTH_SECRET_TOKEN is not set

ok and skipped count as passing; warning, failed and crashed return 503. A check skipped because it doesn’t apply in an environment (the database and queue checks outside production, Redis when the app doesn’t use it) doesn’t fail the endpoint: config/health.php sets treat_skipped_as_failure to false.

Check Status Values:

StatusDescription
okCheck passed
warningCheck passed with warnings
failedCheck failed
crashedCheck threw an exception
skippedCheck was conditionally skipped

In production, health checks run every minute through the scheduler, but email notifications are disabled by default. To receive alerts when checks fail, enable notifications and configure a recipient email address.

Add the following to your .env file:

Terminal window
HEALTH_NOTIFICATIONS_ENABLED=true
HEALTH_NOTIFICATION_EMAIL=your-team@northwestern.edu

While checks keep failing, a notification goes out at most once an hour (notifications.throttle_notifications_for_minutes in config/health.php).

The application uses EloquentHealthResultStore to persist health check results in the database. This enables:

  • Viewing health status in the admin panel’s Platform Overview page
  • Historical tracking for trend analysis
  • Results available immediately after scheduler runs

A daily model:prune for HealthCheckResultHistoryItem in routes/console.php deletes results older than keep_history_for_days (5, in config/health.php).

Create a new check class extending Spatie\Health\Checks\Check:

<?php
namespace App\Domains\Core\Health;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
class CustomServiceCheck extends Check
{
protected ?string $label = 'Custom Service';
public function run(): Result
{
$result = Result::make();
try {
$isHealthy = $this->checkService();
if ($isHealthy) {
return $result
->ok()
->shortSummary('Service operational');
}
return $result
->failed('Service unavailable')
->shortSummary('Service down');
} catch (\Exception $e) {
return $result
->failed($e->getMessage())
->shortSummary('Check failed');
}
}
private function checkService(): bool
{
// Your health check logic here
return true;
}
}

Add your check to HealthServiceProvider:

Health::checks([
// ... existing checks
CustomServiceCheck::new(),
]);

Run checks only in specific environments:

CustomServiceCheck::new()
->if(App::isProduction()),

Or skip checks in certain environments:

CustomServiceCheck::new()
->unless(App::isLocal()),