Health check — an endpoint that tells the load balancer the app is alive
Beginner5 min read·eng-18-011
Concept
Health check — an endpoint or mechanism that reports whether an application (or service) is running correctly. Load balancers, container orchestrators (Kubernetes), and monitoring systems use health checks to route traffic and restart failed services.
Types of health checks:
- Liveness check: Is the application alive? If not, restart it. The simplest check — just
return 200 OK. - Readiness check: Is the application ready to serve traffic? Checks DB connection, cache, queue, dependencies. If not ready, remove from load balancer rotation but don't restart.
- Startup check: Is the application still initializing? Kubernetes uses this to avoid killing slow-starting containers.
What to check in a health endpoint:
- Database connection (can we query?).
- Redis/cache connection.
- Queue connection (are workers running?).
- Disk space available.
- External API dependencies (optional — can cause false positives if third-party is down).
Shallow vs deep health checks:
- Shallow: Just returns 200 if the process is running. Fast. Used by load balancers for basic liveness.
- Deep: Checks all dependencies. Useful for readiness. Can be slow — don't hammer it.
Response format: Return {"status": "ok"} with 200 on success. Return {"status": "degraded", "checks": {...}} with 503 on failure.
Cascading failures: If your health check pings an external API, and that API is down, all your servers fail their health checks → load balancer removes ALL servers → site is down. Be careful about what "unhealthy" means.
Code Example
php
<?php
// routes/web.php or routes/api.php
// SHALLOW liveness check — just is the process running?
Route::get('/ping', fn() => 'pong');
// DEEP readiness check — are dependencies healthy?
Route::get('/health', function () {
$checks = [];
$status = 200;
// Database check
try {
DB::select('SELECT 1');
$checks['database'] = 'ok';
} catch (\Throwable $e) {
$checks['database'] = 'failed: ' . $e->getMessage();
$status = 503;
}
// Redis/cache check
try {
Cache::put('health_check', true, now()->addSeconds(5));
$checks['cache'] = Cache::get('health_check') ? 'ok' : 'failed';
if ($checks['cache'] !== 'ok') $status = 503;
} catch (\Throwable $e) {
$checks['cache'] = 'failed: ' . $e->getMessage();
$status = 503;
}
// Queue check (is the queue connection reachable?)
try {
Queue::size('default'); // check default queue length
$checks['queue'] = 'ok';
} catch (\Throwable $e) {
$checks['queue'] = 'degraded: ' . $e->getMessage();
// Don't fail — queue might be degraded but app still serves requests
}
// Disk space
$freeBytes = disk_free_space(storage_path());
$checks['storage'] = $freeBytes > (100 * 1024 * 1024) ? 'ok' : 'low'; // warn if < 100MB
if ($freeBytes < (10 * 1024 * 1024)) $status = 503; // fail if < 10MB
return response()->json([
'status' => $status === 200 ? 'ok' : 'unhealthy',
'timestamp' => now()->toIso8601String(),
'checks' => $checks,
], $status);
});yaml
# Kubernetes liveness and readiness probes
spec:
containers:
- name: app
image: myapp:latest
livenessProbe:
httpGet:
path: /ping # shallow — just checks process alive
port: 80
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3 # restart after 3 consecutive failures
readinessProbe:
httpGet:
path: /health # deep — checks dependencies
port: 80
initialDelaySeconds: 15
periodSeconds: 15
failureThreshold: 2 # remove from rotation after 2 failures