Health Check
HAWKI includes a comprehensive health check system designed for Docker health checks and monitoring purposes.
Endpoint
Health Check
Endpoint: GET /health
Purpose: Health check suitable for Docker health checks and external monitoring
Authentication: None required
The endpoint uses a two-tier strategy internally (see How It Works) to balance performance and thoroughness.
Success Response (200 OK)
{
"status": "healthy",
"timestamp": "2026-03-10T10:30:45+00:00",
"checks": [
{
"name": "database",
"status": "ok",
"message": "Database connection successful",
"response_time": 5.23
},
{
"name": "cache",
"status": "ok",
"message": "Cache system is operational",
"response_time": 2.15
},
{
"name": "redis",
"status": "ok",
"message": "Redis connection successful",
"response_time": 1.87
},
{
"name": "storage",
"status": "ok",
"message": "Storage is writable",
"response_time": 3.42
}
]
}
Note: On quick checks (see below), only a single database-connectivity result with
"name": "quick_database"is returned inchecks.
Failure Response (503 Service Unavailable)
{
"status": "unhealthy",
"timestamp": "2026-03-10T10:30:45+00:00",
"checks": [
{
"name": "database",
"status": "error",
"message": "Database connection failed"
},
{
"name": "cache",
"status": "ok",
"message": "Cache system is operational",
"response_time": 2.15
},
{
"name": "redis",
"status": "error",
"message": "Redis connection failed"
},
{
"name": "storage",
"status": "ok",
"message": "Storage is writable",
"response_time": 3.42
}
]
}
How It Works
The health check uses a two-tier strategy managed by HealthTimer to avoid overloading the system with expensive checks on every request:
Quick Check
- Runs by default on most requests
- Only verifies basic database connectivity (fast)
- Does not update the healthy/failed state — only a successful deep check resets the state to healthy
- Result contains a single entry in
checks
Deep Check
A deep check is triggered automatically when:
- The previous check failed (deep checks repeat until the system is healthy again), or
- Every 10th quick check (configurable)
A deep check verifies all four components (database, cache, Redis, storage). If all pass, the system is marked as healthy and quick checks resume. If any fail, the system is marked as failed and the next request will trigger another deep check.
State Persistence
The timer state (failure flag and quick-test counter) is stored in a lightweight JSON file in the system temporary directory (hawki_health_timer_marker.json). This intentionally avoids relying on Laravel's cache or database, so the state remains available even when those services are degraded.
Docker Integration
The health check is integrated into the Docker Compose configuration:
healthcheck:
test: curl --fail http://localhost/health || exit 1
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
Configuration Parameters
- interval: Health check runs every 30 seconds
- timeout: Each health check has 10 seconds to complete
- retries: Container is marked unhealthy after 3 consecutive failures
- start_period: Grace period of 30 seconds during container startup
System Components Checked
1. Database
- Verifies database connectivity via PDO
- Executes a
SELECT 1query to ensure the database is responsive - Measures response time
2. Cache
- Tests cache read/write/delete operations
- Verifies data integrity (written value matches retrieved value)
- Measures response time
- Works with any configured cache driver (database, redis, file, etc.)
3. Redis
- Checks Redis connectivity
- Executes a
PINGcommand - Measures response time
4. Storage
- Verifies that
storage/framework/cacheis writable - Creates and deletes a temporary test file
- Measures response time
Monitoring
You can monitor the health status of your containers using:
# Check health status
docker compose ps
# View health check logs
docker inspect --format='{{json .State.Health}}' hawki-app | jq
# Follow health check events
docker events --filter event=health_status
Production Deployment
For production deployments, consider:
- External Monitoring: Use the
/healthendpoint with external monitoring tools (Prometheus, Datadog, etc.) - Alerting: Set up alerts when health checks return
503 - Load Balancers: Configure load balancers to use
/healthfor service availability checks - Logging: Monitor health check failures in application logs
Troubleshooting
Container Marked as Unhealthy
- Check Docker logs:
docker compose logs app - Access the health endpoint directly:
curl http://localhost/health - Verify all services are running:
docker compose ps - Check Laravel logs:
storage/logs/laravel.log
Common Issues
Database Connection Failed
- Verify MySQL container is running
- Check database credentials in
.env - Ensure database migrations have run
Redis Connection Failed
- Verify Redis container is running
- Check Redis configuration in
.env - Test Redis connectivity:
docker compose exec redis redis-cli ping
Storage Check Failed
- Verify proper file permissions on
storage/framework/cache - Check available disk space
- Ensure storage directories exist
Timer state out of sync
- The timer state file is stored in the system temp directory as
hawki_health_timer_marker.json - Delete the file to reset the timer to a clean state:
rm $(php -r 'echo sys_get_temp_dir();')/hawki_health_timer_marker.json
Custom Health Checks
Custom checks are added by listening to the HealthCheckEvent, which is dispatched during every deep check. There is no need to modify the core HealthChecker class.
How It Works
After the four built-in checks (database, cache, Redis, storage) finish, HealthChecker::deepCheck() dispatches a HealthCheckEvent carrying the initial HealthCheckResultCollection. Listeners receive the event and can append their own HealthCheckResult objects via $event->addResult(). The final collection — including all custom results — is then used to determine the overall health status.
If a custom result's checkName matches an existing entry, it overwrites that entry, which allows overriding built-in checks if needed.
Creating a Listener
1. Create the listener class
<?php
declare(strict_types=1);
namespace App\Listeners;
use App\Events\HealthCheckEvent;
use App\Services\System\Health\Value\HealthCheckResult;
class CustomServiceHealthListener
{
public function handle(HealthCheckEvent $event): void
{
try {
// Your custom check logic here
// e.g. ping an external API, verify a queue worker is alive, etc.
$event->addResult(new HealthCheckResult(
checkName: 'custom_service',
status: HealthCheckResult::STATUS_OK,
message: 'Custom service is operational',
));
} catch (\Throwable $e) {
$event->addResult(new HealthCheckResult(
checkName: 'custom_service',
status: HealthCheckResult::STATUS_ERROR,
message: 'Custom service check failed: ' . $e->getMessage(),
));
}
}
}
2. Register the listener
Add the mapping in app/Providers/EventServiceProvider.php (or wherever your project registers listeners):
use App\Events\HealthCheckEvent;
use App\Listeners\CustomServiceHealthListener;
protected $listen = [
HealthCheckEvent::class => [
CustomServiceHealthListener::class,
],
];
The custom result will automatically appear in the /health response alongside the built-in checks:
{
"name": "custom_service",
"status": "ok",
"message": "Custom service is operational",
"response_time": null
}
Tip: Wrap your check logic in a
trackTime()equivalent (a simplemicrotimecall) and pass the result as theresponseTimeargument if you want response-time data to appear in the output.