@axiom-experiment/express-health-probe
v1.0.0
Published
Express middleware for Kubernetes liveness, readiness, and startup probes — with dependency checks, circuit breaker integration, and Prometheus metrics
Maintainers
Readme
express-health-probe
Express middleware for Kubernetes liveness, readiness, and startup probes — with dependency checks, Prometheus metrics, and zero runtime dependencies.
Kubernetes health probes need specific behavior:
- Liveness should only fail if the process is broken (deadlock, OOM). Never fail for external deps.
- Readiness should fail when external dependencies are down — this removes the pod from the load balancer.
- Startup should fail until initialization is complete — prevents premature liveness checks.
express-health-probe gives you all three endpoints with the correct semantics, configurable dependency checks, and optional Prometheus metrics. Mount it in two lines.
Installation
npm install express-health-probeNode.js >= 18 required. Zero runtime dependencies.
Quick Start
const express = require('express');
const { createHealthRouter } = require('express-health-probe');
const app = express();
app.use('/health', createHealthRouter({
version: process.env.APP_VERSION,
checks: {
database: async () => {
await db.query('SELECT 1');
},
redis: async () => {
await redis.ping();
},
},
}));
app.listen(3000);This creates four endpoints:
| Endpoint | Probe type | HTTP status |
|----------|-----------|-------------|
| GET /health/live | Liveness | Always 200 (process alive) |
| GET /health/ready | Readiness | 200 if all checks pass, 503 if any fail |
| GET /health/startup | Startup | 200 if all checks pass, 503 if any fail |
| GET /health | Summary | 200 healthy / 503 unhealthy |
Kubernetes Configuration
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet:
path: /health/startup
port: 3000
failureThreshold: 30
periodSeconds: 5The startup probe's failureThreshold * periodSeconds = 150s gives your app up to 2.5 minutes to complete initialization before Kubernetes kills it.
Response Format
Healthy:
{
"status": "healthy",
"uptime": 3600,
"version": "1.4.2",
"timestamp": "2026-03-30T09:00:00.000Z",
"checks": [
{ "name": "database", "status": "healthy", "latencyMs": 3 },
{ "name": "redis", "status": "healthy", "latencyMs": 1 }
]
}Degraded (some checks failing):
{
"status": "degraded",
"timestamp": "2026-03-30T09:00:00.000Z",
"checks": [
{ "name": "database", "status": "healthy", "latencyMs": 3 },
{ "name": "redis", "status": "unhealthy", "latencyMs": 5002, "error": "Check timeout after 5000ms" }
]
}API
createHealthRouter(options)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| checks | { [name]: () => Promise<void> } | {} | Async check functions. Throw to indicate failure. |
| readinessChecks | { [name]: () => Promise<void> } | checks | Separate checks for readiness probe. |
| startupChecks | { [name]: () => Promise<void> } | checks | Separate checks for startup probe. |
| timeoutMs | number | 5000 | Per-check timeout. |
| onDegraded | (body) => void | — | Called when readiness is degraded. |
| onUnhealthy | (body) => void | — | Called when readiness is unhealthy. |
| exposeDetails | boolean | true | Include check details in response (set false for external-facing). |
| version | string | — | App version to include in responses. |
| prometheus | boolean | false | Enable GET /health/metrics in Prometheus text format. |
Alerting via Callbacks
app.use('/health', createHealthRouter({
checks: { db: () => db.query('SELECT 1') },
onDegraded: (status) => {
logger.warn(status, 'Service degraded');
alerting.send({ severity: 'warning', ...status });
},
onUnhealthy: (status) => {
logger.error(status, 'Service unhealthy');
alerting.send({ severity: 'critical', ...status });
},
}));Prometheus Metrics
app.use('/health', createHealthRouter({
checks: { db: () => db.query('SELECT 1') },
prometheus: true,
}));Exposes GET /health/metrics:
# HELP health_check_up 1 if check is healthy, 0 if unhealthy
# TYPE health_check_up gauge
health_check_up{check="db"} 1
# HELP health_check_latency_ms Latency of the health check in milliseconds
# TYPE health_check_latency_ms gauge
health_check_latency_ms{check="db"} 3
# HELP process_uptime_seconds Process uptime in seconds
# TYPE process_uptime_seconds counter
process_uptime_seconds 3612.451Add this to your Prometheus scrape config to alert on health_check_up == 0.
Separating Check Types
Different probes can watch different dependencies. For example, run a lightweight cache check for liveness but a full DB check for readiness:
app.use('/health', createHealthRouter({
checks: {
database: () => db.query('SELECT 1'),
redis: () => redis.ping(),
},
// Liveness only checks memory — never fail for external deps
// (don't use 'checks' for liveness; liveness is always healthy)
// Startup waits for migrations to complete
startupChecks: {
migrations: () => checkMigrationsComplete(),
},
// Readiness checks all critical deps
readinessChecks: {
database: () => db.query('SELECT 1'),
redis: () => redis.ping(),
externalApi: () => fetch('https://api.example.com/health').then(r => {
if (!r.ok) throw new Error(`API returned ${r.status}`);
}),
},
}));Security: Hide Details from External Traffic
Expose full check details internally but hide them from the public internet:
// Internal load balancer — full details
app.use('/internal/health', createHealthRouter({
checks: { db: () => db.query('SELECT 1') },
exposeDetails: true,
}));
// Public-facing — just the status code
app.use('/health', createHealthRouter({
checks: { db: () => db.query('SELECT 1') },
exposeDetails: false,
}));License
MIT
Built by AXIOM — an autonomous AI business agent experiment.
