Health checks are essential endpoints that allow monitoring systems, load balancers, and orchestration platforms to verify your Express application is running correctly.
Why Health Checks?
Health check endpoints provide several critical benefits:
Load balancer integration - Direct traffic only to healthy instances
Container orchestration - Kubernetes and Docker Swarm use health checks
Monitoring and alerting - Track application availability
Graceful deployments - Ensure new instances are ready before routing traffic
Dependency verification - Check database and service connections
Basic Health Check
Start with a simple endpoint that returns a successful status.
const express = require ( 'express' );
const app = express ();
app . get ( '/health' , ( req , res ) => {
res . status ( 200 ). json ({ status: 'ok' });
});
app . listen ( 3000 );
Place health checks before authentication middleware so they’re always accessible.
Liveness vs Readiness
Implement different health checks for different purposes.
Liveness Probe
Checks if the application is alive and should be restarted if failing. app . get ( '/health/live' , ( req , res ) => {
// Basic check - is the process running?
res . status ( 200 ). json ({
status: 'ok' ,
timestamp: new Date (). toISOString ()
});
});
Readiness Probe
Checks if the application is ready to receive traffic. let isReady = false ;
// Set ready after initialization
async function initialize () {
await connectToDatabase ();
await loadConfiguration ();
isReady = true ;
}
app . get ( '/health/ready' , ( req , res ) => {
if ( isReady ) {
res . status ( 200 ). json ({ status: 'ready' });
} else {
res . status ( 503 ). json ({ status: 'not ready' });
}
});
initialize ();
Use correct status codes - Return 200 for healthy, 503 for unhealthy. Many load balancers depend on this.
Comprehensive Health Check
Check all critical dependencies and return detailed status.
Basic Implementation
Advanced Implementation
app . get ( '/health' , async ( req , res ) => {
const health = {
status: 'ok' ,
timestamp: new Date (). toISOString (),
uptime: process . uptime (),
checks: {}
};
try {
// Check database
await db . query ( 'SELECT 1' );
health . checks . database = 'ok' ;
} catch ( err ) {
health . checks . database = 'error' ;
health . status = 'error' ;
}
try {
// Check Redis
await redis . ping ();
health . checks . redis = 'ok' ;
} catch ( err ) {
health . checks . redis = 'error' ;
health . status = 'error' ;
}
const statusCode = health . status === 'ok' ? 200 : 503 ;
res . status ( statusCode ). json ( health );
});
Graceful Shutdown
Handle shutdown signals properly to avoid dropping connections.
const express = require ( 'express' );
const app = express ();
let isShuttingDown = false ;
let server ;
// Health check reflects shutdown state
app . get ( '/health' , ( req , res ) => {
if ( isShuttingDown ) {
res . status ( 503 ). json ({ status: 'shutting down' });
} else {
res . status ( 200 ). json ({ status: 'ok' });
}
});
app . get ( '/' , ( req , res ) => {
res . send ( 'Hello World' );
});
server = app . listen ( 3000 , () => {
console . log ( 'Server started on port 3000' );
});
// Handle shutdown signals
process . on ( 'SIGTERM' , gracefulShutdown );
process . on ( 'SIGINT' , gracefulShutdown );
function gracefulShutdown () {
console . log ( 'Received shutdown signal' );
isShuttingDown = true ;
server . close (() => {
console . log ( 'Server closed' );
// Close database connections
db . close (() => {
console . log ( 'Database connection closed' );
process . exit ( 0 );
});
});
// Force shutdown after 30 seconds
setTimeout (() => {
console . error ( 'Forced shutdown after timeout' );
process . exit ( 1 );
}, 30000 );
}
Set isShuttingDown to true immediately so health checks fail and load balancers stop sending traffic.
Detailed System Metrics
Include system information in health checks for monitoring.
const os = require ( 'os' );
app . get ( '/health/detailed' , async ( req , res ) => {
const memUsage = process . memoryUsage ();
const health = {
status: 'ok' ,
timestamp: new Date (). toISOString (),
// Process metrics
process: {
uptime: process . uptime (),
pid: process . pid ,
memory: {
heapUsed: Math . round ( memUsage . heapUsed / 1024 / 1024 ) + 'MB' ,
heapTotal: Math . round ( memUsage . heapTotal / 1024 / 1024 ) + 'MB' ,
rss: Math . round ( memUsage . rss / 1024 / 1024 ) + 'MB'
},
cpu: process . cpuUsage ()
},
// System metrics
system: {
platform: os . platform (),
arch: os . arch (),
cpus: os . cpus (). length ,
loadAverage: os . loadavg (),
totalMemory: Math . round ( os . totalmem () / 1024 / 1024 ) + 'MB' ,
freeMemory: Math . round ( os . freemem () / 1024 / 1024 ) + 'MB'
},
// Dependency checks
checks: {}
};
// Check dependencies
try {
await db . query ( 'SELECT 1' );
health . checks . database = { status: 'ok' };
} catch ( err ) {
health . checks . database = { status: 'error' , error: err . message };
health . status = 'degraded' ;
}
const statusCode = health . status === 'ok' ? 200 : 503 ;
res . status ( statusCode ). json ( health );
});
Kubernetes Integration
Configure Kubernetes to use your health checks.
apiVersion : v1
kind : Pod
metadata :
name : express-app
spec :
containers :
- name : express-app
image : express-app:latest
ports :
- containerPort : 3000
# Liveness probe - restart if failing
livenessProbe :
httpGet :
path : /health/live
port : 3000
initialDelaySeconds : 30
periodSeconds : 10
timeoutSeconds : 5
failureThreshold : 3
# Readiness probe - remove from service if failing
readinessProbe :
httpGet :
path : /health/ready
port : 3000
initialDelaySeconds : 5
periodSeconds : 5
timeoutSeconds : 3
failureThreshold : 2
# Startup probe - give extra time for initial startup
startupProbe :
httpGet :
path : /health/live
port : 3000
initialDelaySeconds : 0
periodSeconds : 5
timeoutSeconds : 3
failureThreshold : 30
Load Balancer Configuration
upstream express_backend {
server app1.example.com:3000 max_fails = 3 fail_timeout=30s;
server app2.example.com:3000 max_fails = 3 fail_timeout=30s;
server app3.example.com:3000 max_fails = 3 fail_timeout=30s;
}
server {
listen 80 ;
server_name example.com;
location /health {
access_log off ;
return 200 "OK" ;
}
location / {
proxy_pass http://express_backend;
proxy_set_header Host $ host ;
proxy_set_header X-Real-IP $ remote_addr ;
proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for ;
# Health check configuration
health_check interval=10s fails=3 passes=2 uri=/health;
}
}
AWS Application Load Balancer
{
"HealthCheckEnabled" : true ,
"HealthCheckIntervalSeconds" : 30 ,
"HealthCheckPath" : "/health" ,
"HealthCheckProtocol" : "HTTP" ,
"HealthCheckTimeoutSeconds" : 5 ,
"HealthyThresholdCount" : 2 ,
"UnhealthyThresholdCount" : 3 ,
"Matcher" : {
"HttpCode" : "200"
}
}
version : '3.8'
services :
app :
build : .
ports :
- "3000:3000"
healthcheck :
test : [ "CMD" , "wget" , "--quiet" , "--tries=1" , "--spider" , "http://localhost:3000/health" ]
interval : 30s
timeout : 10s
retries : 3
start_period : 40s
Monitoring Integration
Integrate with monitoring systems for alerting.
const express = require ( 'express' );
const client = require ( 'prom-client' );
const app = express ();
// Create metrics
const register = new client . Registry ();
const healthCheckGauge = new client . Gauge ({
name: 'app_health_status' ,
help: 'Application health status (1 = healthy, 0 = unhealthy)' ,
registers: [ register ]
});
const dependencyGauge = new client . Gauge ({
name: 'app_dependency_status' ,
help: 'Dependency health status' ,
labelNames: [ 'dependency' ],
registers: [ register ]
});
// Health check with metrics
app . get ( '/health' , async ( req , res ) => {
const health = { status: 'ok' , checks: {} };
let isHealthy = true ;
// Check database
try {
await db . query ( 'SELECT 1' );
health . checks . database = 'ok' ;
dependencyGauge . set ({ dependency: 'database' }, 1 );
} catch ( err ) {
health . checks . database = 'error' ;
dependencyGauge . set ({ dependency: 'database' }, 0 );
isHealthy = false ;
}
// Update overall health metric
healthCheckGauge . set ( isHealthy ? 1 : 0 );
if ( isHealthy ) {
res . status ( 200 ). json ( health );
} else {
health . status = 'error' ;
res . status ( 503 ). json ( health );
}
});
// Prometheus metrics endpoint
app . get ( '/metrics' , async ( req , res ) => {
res . set ( 'Content-Type' , register . contentType );
res . end ( await register . metrics ());
});
Best Practices
Don’t require authentication - Health checks must be accessible to load balancers and monitoring systems.
Keep it fast - Health checks should complete in under 1 second to avoid timeout issues.
Use appropriate status codes - 200 for healthy, 503 for unhealthy, 429 if rate limited.
Implement both liveness and readiness probes
Check critical dependencies only
Return quickly to avoid timeouts
Include version information
Log health check failures
Don’t perform expensive operations
Handle graceful shutdown
Use consistent response formats
Monitor health check metrics
Set appropriate timeouts and intervals
Testing Health Checks
Test your health checks to ensure they work correctly.
const request = require ( 'supertest' );
const app = require ( './app' );
describe ( 'Health Checks' , () => {
it ( 'should return 200 when healthy' , async () => {
const response = await request ( app )
. get ( '/health' )
. expect ( 200 );
expect ( response . body . status ). toBe ( 'ok' );
});
it ( 'should return 503 when database is down' , async () => {
// Mock database failure
jest . spyOn ( db , 'query' ). mockRejectedValue ( new Error ( 'Connection failed' ));
const response = await request ( app )
. get ( '/health' )
. expect ( 503 );
expect ( response . body . status ). toBe ( 'error' );
expect ( response . body . checks . database ). toBe ( 'error' );
});
it ( 'should return 503 during shutdown' , async () => {
app . isShuttingDown = true ;
await request ( app )
. get ( '/health' )
. expect ( 503 );
});
});
Next Steps