Skip to main content
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.
1

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()
  });
});
2

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.
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;
    }
}
{
  "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

Build docs developers (and LLMs) love