Building Reliable Health Checks for Production Services
Design resilient health checks for production services. Master probe taxonomy, prevent cascading restart storms and probe flapping, and implement RFC-compliant checks.

Building Reliable Health Checks for Production Services
In distributed production systems, container orchestrators (such as Kubernetes, Nomad, or Amazon ECS) and reverse proxies (such as Envoy, NGINX, or cloud load balancers) constantly poll service instances to make two critical decisions:
- Is this process broken beyond recovery, requiring an immediate restart?
- Can this specific instance handle an incoming customer request right now?
When teams conflate these two questions into a single naive /health endpoint that pings an external database, they inadvertently plant one of the most destructive architectural failure modes in modern cloud infrastructure: the cascading reboot storm. A transient latency spike in a downstream database causes health checks across every container replica to fail simultaneously. The orchestrator responds by killing and restarting every instance at the exact same moment. When all replicas reboot concurrently, their initialization routines flood the already struggling database with thousands of new TCP connections, guaranteeing the cluster never recovers.
A production health check is not a passive status badge; it is an active control-plane feedback loop. When configured correctly, it isolates failures, enables zero-downtime rolling deployments, sheds load safely, and prevents self-inflicted denial-of-service outages. When configured incorrectly, it magnifies minor blips into total systemic collapse.
This guide explores the engineering principles, probe taxonomies, failure cascade mechanics, protocol standards, and concrete implementations required to build bulletproof health checking systems.
1. The Core Probe Taxonomy: Startup, Liveness, and Readiness
Modern orchestrators divide health inspection into three distinct probes. Conflating their responsibilities is the root cause of the vast majority of health check operational incidents.
Pod Initialization & Probe Lifecycle:
Pod Created
│
▼
Container Started ────────────────────────────────────────┐
│ │
▼ │
[Startup Probe Polling] │
│ │
├── Fails (exceeds failureThreshold) ───────────────> │ Kill & Restart Container
│ │ (Subject to Pod restartPolicy)
▼ Succeeds (First 200-399 OK) │
Startup Complete: Enable Liveness & Readiness │
│ │
├─────────────────────────────┬───────────────────────┤
│ │ │
▼ ▼ │
[Liveness Probe Loop] [Readiness Probe Loop] │
(Tests In-Process Health) (Tests Traffic Viability) │
│ │ │
├─ Consecutive Failures ├─ Consecutive Failures │
│ (failureThreshold: 3) │ (failureThreshold: 3)│
│ │ │
▼ ▼ │
SIGTERM Dispatched Pod Marked Unready │
Wait terminationGracePeriod Removed from EndpointSlice │
SIGKILL if still alive Traffic Ceases Immediately │
Container Restarted (Container Remains Running) │
│ │ │
│ ▼ │
│ Recovers (successThreshold: 1)
│ Re-added to EndpointSlice │
│ Traffic Resumes │
└─────────────────────────────────────────────────────┘
1.1 Startup Probes: Guarding Cold Starts
Many backend services require significant time to initialize before they can safely process requests. Tasks such as hydrating in-memory caches, JIT compilation in runtime environments like the JVM, running database schema preflights, or loading multi-gigabyte machine learning model weights can take anywhere from thirty seconds to several minutes.
Before startup probes were introduced in Kubernetes v1.18, teams had to compensate by inflating the initialDelaySeconds on their liveness probes. However, setting an initial delay of two minutes meant that if a container experienced a runtime deadlock after booting, the orchestrator would wait two full minutes before initiating a recovery restart.
A startup probe solves this dilemma:
- Behavior: It begins polling immediately upon container creation. While the startup probe is executing, all liveness and readiness probes are disabled.
- Success Action: As soon as the startup probe returns a successful HTTP status (200–399), the startup phase terminates, and the orchestrator immediately activates concurrent liveness and readiness probing.
- Failure Action: If the startup probe does not succeed within its allocated budget (
periodSeconds × failureThreshold), the container is killed and restarted according to the pod'srestartPolicy.
This allows you to assign a generous startup window (e.g., up to 150 seconds) without degrading fast runtime crash detection once the service is running.
1.2 Liveness Probes: Detecting Unrecoverable Process Death
The sole responsibility of a liveness probe is to answer: Is the application process running and capable of making forward progress, or is it stuck in an unrecoverable state?
Unrecoverable states include:
- A deadlocked thread pool where worker threads are blocked on mutual mutex locks.
- An event loop that has halted or experienced a fatal internal exception.
- Severe memory corruption where the process can no longer allocate buffers or process OS signals.
The Failure Consequence
When a liveness probe fails consecutively past its failureThreshold:
- The orchestrator's local agent (e.g.,
kubelet) sends aSIGTERMsignal to the container process. - It waits for the duration specified in
terminationGracePeriodSeconds(default: 30s). - If the process has not terminated, it dispatches
SIGKILLto force process destruction. - It creates a brand-new container instance.
[!CAUTION] The Critical Invariant of Liveness Checks: A liveness probe must NEVER check downstream network dependencies such as databases, caches, messaging brokers, or third-party APIs.
If your database suffers a temporary network partition or reaches its connection ceiling, killing your web service does not fix the database. It causes every single replica in your fleet to reboot at once, turning a minor database degradation into a complete application-tier outage.
1.3 Readiness Probes: Controlling Traffic Admission
A readiness probe answers a fundamentally different question: Is this container currently able and willing to accept customer traffic?
A service instance might be running perfectly healthy internal code, but temporarily unable to serve incoming requests because:
- Its local database connection pool is currently saturated and waiting for idle connections.
- It is actively synchronizing a distributed transaction log or warming an internal LRU cache.
- A mandatory external dependency (e.g., the primary datastore) is temporarily unreachable.
- It is experiencing transient CPU throttling and needs to shed traffic to drain its internal buffer.
The Failure Consequence
When a readiness probe fails:
- The container process is NOT terminated. No
SIGTERMis sent. - The orchestrator's control plane updates the service discovery registry (in Kubernetes, the
EndpointSlicecontroller removes the Pod's IP from the active endpoints). - Ingress controllers and internal load balancers immediately stop forwarding new client requests to this replica.
- The container continues running in the background. It can drain existing in-flight connections, finish processing background jobs, re-establish lost database connections, and warm its caches without interruption.
- Once the readiness probe succeeds again (governed by
successThreshold, typically 1), the controller adds the pod IP back to theEndpointSlice, and live traffic resumes smoothly.
Kubernetes EndpointSlice Traffic Routing:
[ Ingress / Service ]
│
▼
[ EndpointSlice Controller ]
Evaluates Readiness Probes
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
[ Pod 1 ] [ Pod 2 ] [ Pod 3 ]
Readiness: PASS Readiness: FAIL Readiness: PASS
(HTTP 200) (HTTP 503) (HTTP 200)
│ │ │
▼ ▼ ▼
EndpointSlice: EndpointSlice: EndpointSlice:
Active (Ready) EVICTED (Unready) Active (Ready)
│ │ │
▼ ▼ ▼
Receives User DROPPED FROM Receives User
Traffic ROUTING Traffic
(Zero 502 Errors)
2. Probe Comparison Across Six Engineering Dimensions
To prevent configuration errors during deployment, architects must evaluate each probe across six operational criteria:
| Dimension | Startup Probe (/startupz or /livez) | Liveness Probe (/livez) | Readiness Probe (/readyz) |
| :--- | :--- | :--- | :--- |
| 1. Operational Question | Has the process finished cold bootstrapping and initialization? | Is the process loop responsive, or is it frozen in a fatal deadlock? | Can this instance service incoming client requests right now? |
| 2. Failure Reaction | Kubelet destroys and re-creates the container. | Kubelet issues SIGTERM, waits grace period, issues SIGKILL, restarts container. | Pod IP removed from EndpointSlice routing. Container process remains running. |
| 3. Dependency Scope | Internal startup dependencies (schema validation, local model loading). | Zero external dependencies. Strictly in-process event loop and thread checks. | Essential dependencies (DB connection pool, Redis cache socket). |
| 4. Evaluation Lifecycle | Executes exclusively during container boot; halts once successful. | Executes continuously throughout container lifespan after startup succeeds. | Executes continuously throughout container lifespan after startup succeeds. |
| 5. Tolerance Guidelines | High failure threshold (e.g., period: 5s, failureThreshold: 30, allowing 150s). | Moderate period, tight timeout (e.g., period: 10s, timeout: 2s, failureThreshold: 3). | Fast period, tight timeout (e.g., period: 5s, timeout: 2s, failureThreshold: 3). |
| 6. Failure Mode Mitigated | Premature kills of slow-starting services during horizontal scaling. | Zombie processes, memory leaks freezing execution loops, mutex deadlocks. | Routing user requests to cold pods, saturated pools, or partitioned nodes. |
3. Deep vs. Shallow Health Checks
Designing health check endpoints requires categorizing them by execution depth and caller identity.
3.1 Shallow Probes (/livez)
Shallow probes are built specifically for orchestrator liveness checks. They should execute in microseconds and consume negligible CPU.
- What they inspect:
- Can the HTTP server accept an incoming TCP socket?
- Is the runtime event loop or main thread progressing (e.g., measuring event loop lag via
setImmediateor inspecting an internal heartbeat atomic variable)? - Is process resident memory below an absolute fatal threshold?
- What they must omit:
- No database ping queries (
SELECT 1). - No cache calls (
PING). - No disk I/O operations unless local disk is the service's primary storage engine.
- No outbound HTTP requests to peer services.
- No database ping queries (
3.2 Deep Probes (/readyz)
Deep probes are designed for readiness controllers. They evaluate whether the instance possesses the local connections and resources required to fulfill client requests.
- What they inspect:
- Database connection pool state: Does the pool have idle connections available, or is the waiting queue within an acceptable ceiling?
- Cache connectivity: Is the socket connection to Redis/Memcached active?
- Local worker queues: Are in-memory dispatch channels below saturation thresholds?
- Timeout and Fallback Rules:
- Every component check inside
/readyzmust enforce an aggressive timeout (e.g., 500 ms) using an abort signal. A slow database query must never cause the health endpoint itself to block indefinitely. - Optional or non-critical dependencies (e.g., analytics pipeline, recommendation engine, asynchronous audit loggers) must never fail the readiness check if the application has a fallback or degraded mode available.
- Every component check inside
3.3 Diagnostic / Administrative Endpoints (/healthz/status)
Diagnostic endpoints are intended for human operators, SRE incident responders, and observability scrapers (like Prometheus).
- Characteristics:
- Expose deep telemetry: exact connection pool numbers (
idle,active,max,waiting), upstream circuit breaker states, replication lag in milliseconds, JVM heap allocation, and garbage collection pauses. - Security Requirement: Because diagnostic endpoints reveal internal infrastructure topologies, hostnames, and pool metrics, they must be protected by authentication (e.g., internal mTLS, bearer tokens, or bound strictly to an internal loopback/management port).
- Isolation: Diagnostic endpoints must never be used as the target for high-frequency load balancer polling.
- Expose deep telemetry: exact connection pool numbers (
4. Failure Cascades and Operational Anti-Patterns
Health check misconfigurations are among the most common root causes of multi-hour outages in modern cloud platforms. Understanding how these anti-patterns develop allows teams to build safeguards before going to production.
The Cascading Reboot Storm (Flawed Architecture):
Downstream DB Latency Spike
│
▼
All Pods fail Liveness Probes (Liveness checking DB)
│
▼
Kubelet kills all containers across all nodes simultaneously
│
▼
Containers reboot simultaneously -> All attempt DB connection initialization
│
▼
Massive connection thundering herd crashes the database
│
▼
Pods fail Liveness again -> Permanent Cluster-Wide Crash Loop
────────────────────────────────────────────────────────────────────────
The Resilient Architecture (Readiness Decoupling):
Downstream DB Latency Spike
│
├─────────────────────────────────────────┐
▼ ▼
Liveness Probe evaluates /livez Readiness Probe evaluates /readyz
(In-process check: event loop OK) (Pool check: DB connection timeout)
│ │
▼ ▼
Liveness PASSES (HTTP 200) Readiness FAILS (HTTP 503)
Containers remain running! Pod removed from EndpointSlice
Zero container restarts! Traffic diverted to surviving nodes
│ │
└────────────────────┬────────────────────┘
│
▼
Database recovers as customer traffic drops;
Pod connection pools remain intact;
Readiness flips to PASS -> Traffic seamlessly resumes.
4.1 Anti-Pattern 1: The Cascading Reboot Storm
Consider a web application with 100 container replicas backed by a relational database cluster.
- The database experiences a transient 5-second query latency spike due to a sudden query plan regression or disk lock contention.
- Because the application's
/livezprobe executesSELECT 1, every probe query across all 100 pods exceeds its 1-second timeout. - The kubelet on every node detects three consecutive probe failures. It dispatches
SIGTERMand kills all 100 pods simultaneously. - Kubelet immediately restarts 100 replacement containers.
- As all 100 containers boot concurrently, their application bootstrap logic opens their minimum configured database pool connections (e.g., 20 connections per pod = 2,000 concurrent connection attempts).
- The database, already under severe strain, is hit with a thundering herd of 2,000 TCP handshakes and authentication exchanges. Database CPU hits 100%, and connection memory explodes.
- The newly booted pods try to run their liveness checks, which time out again against the crushed database.
- The cluster enters a permanent death spiral of restarts. The only resolution is manually scaling the web application deployment to zero replicas, waiting for the database to stabilize, and gingerly scaling back up.
The Fix: Keep liveness probes purely in-process. Move database dependency checks strictly to readiness probes. When the database slows down, readiness probes fail. The orchestrator stops sending user traffic to the pods, but keeps the processes alive. No containers reboot, no thundering herds occur on connection pools, and as soon as the database recovers, traffic resumes instantly.
4.2 Anti-Pattern 2: Self-Inflicted Health Check Denial of Service
In large clusters, health checking traffic can represent a significant percentage of total database query load if probes are evaluated by querying the network every few seconds.
Suppose an application has 200 replicas. The readiness probe is configured with periodSeconds: 2, and each probe runs SELECT 1 on the primary database:
The database is forced to execute 100 queries per second 24 hours a day, 7 days a week, merely verifying that pods are alive. If the cluster scales up to 1,000 pods during high traffic, health check overhead hits 500 queries per second.
The Fix:
- In-Memory TTL Caching: Cache health check evaluation results in application memory for a brief window (e.g., 2,000 ms). If multiple orchestrators or concurrent threads poll
/readyzwithin that window, return the cached result instantly without touching the network. - Inspect Pool Metrics Locally: Instead of dispatching a network SQL query across the wire, inspect the in-memory state of the connection pool driver. If the pool reports active connections, zero broken sockets, and a non-exhausted queue, the datastore connection is healthy.
4.3 Anti-Pattern 3: Probe Flapping and Cascading Load Shock
Probe flapping occurs when an orchestrator removes an instance from routing too quickly, causing the remaining healthy instances to become overloaded and fail in sequence.
If failureThreshold is set to 1:
- Pod A experiences a brief 100 ms stop-the-world garbage collection pause right when a probe arrives.
- The probe fails. Kubelet instantly evicts Pod A from the load balancer.
- 100% of incoming customer traffic is now divided among the remaining pods (Pods B and C).
- The sudden 50% increase in request volume pushes Pod B into a garbage collection pause. Pod B's probe fails and is evicted.
- Pod C absorbs all traffic, collapses under the load, and fails. The entire cluster goes dark.
The Fix: Multi-sample smoothing. Set failureThreshold: 3 and periodSeconds: 5. A pod must fail three consecutive checks spanning 15 seconds before it is marked unready. This eliminates false positives caused by minor GC pauses, network jitter, or CPU scheduling delays.
4.4 Anti-Pattern 4: Connection Pool Exhaustion from Un-Timeouted Checks
If a health check route borrows a connection from the same pool used by customer requests, a busy service can inadvertently kill itself.
Under heavy traffic, all 20 connections in the pool are checked out by long-running HTTP handler threads. An incoming /readyz request arrives and calls pool.acquire(). Because the pool is exhausted, the health check thread blocks, waiting for a connection to be returned.
If the health check does not specify an explicit local timeout, it hangs until the orchestrator's probe timeout (e.g., 2 seconds) triggers. The probe fails, not because the database was broken, but because the health check itself was stuck in the pool's wait queue.
The Fix:
- Never block on connection acquisition for health checks without an explicit abort timeout (e.g.,
500ms). - Prefer inspecting pool statistics (
pool.idleCount > 0) over acquiring physical connections. - If a ping query is mandatory, maintain a dedicated single reserve connection specifically for operational telemetry, isolated from customer traffic.
5. Standardizing Health Check Protocols: IETF RFC Specification
Standardizing the HTTP response format across all microservices ensures that load balancers, monitoring daemons, and developer tools can parse health diagnostics uniformly.
The IETF draft specification (draft-inadarei-api-health-check-06) establishes the standard MIME type application/health+json and defines structured statuses:
5.1 Status Semantics
"pass": The service and all examined components are operating within normal parameters."warn": The service is functional, but one or more sub-components are degraded or approaching capacity limits (e.g., connection pool usage above 85%, or elevated event loop lag)."fail": The service or a critical dependency has failed. The instance cannot reliably serve traffic.
5.2 HTTP Status Code Resolution
- Status
"pass"→ HTTP 200 OK - Status
"warn"→ HTTP 200 OK: Returning 200 ensures upstream reverse proxies and load balancers continue routing traffic to the node while alerting monitoring dashboards. - Status
"fail"→ HTTP 503 Service Unavailable: Returning 503 instructs reverse proxies and ingress controllers to immediately decouple the node from upstream routing. Furthermore, standard HTTP caches and CDNs will not cache 503 responses as valid payload representations.
5.3 Mandatory Headers
Every health check response must return:
Content-Type: application/health+json
Cache-Control: no-cache, no-store, must-revalidate
5.4 Example RFC-Compliant Payload
{
"status": "pass",
"version": "1.4.2",
"releaseId": "prod-commit-a4f912c",
"checks": {
"datastore:primary-pg": [
{
"componentType": "datastore",
"status": "pass",
"observedValue": 14,
"observedUnit": "idle-connections",
"output": "Database connection pool healthy",
"time": "2025-11-17T10:14:02.120Z"
}
],
"cache:redis-cluster": [
{
"componentType": "cache",
"status": "pass",
"observedValue": 1.4,
"observedUnit": "ms",
"output": "Redis ping latency normal",
"time": "2025-11-17T10:14:02.122Z"
}
]
}
}
6. Production Kubernetes Deployment Specification
Below is a production-grade Kubernetes Deployment manifest illustrating how startup, liveness, and readiness probes must be configured alongside graceful termination hooks:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
labels:
app.kubernetes.io/name: order-service
app.kubernetes.io/component: backend
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: order-service
template:
metadata:
labels:
app.kubernetes.io/name: order-service
spec:
# Allow in-flight requests and preStop hook to complete cleanly
terminationGracePeriodSeconds: 45
containers:
- name: order-service
image: ghcr.io/example/order-service:v1.4.2
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
protocol: TCP
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "1024Mi"
# 1. STARTUP PROBE: Protects slow initial bootstrapping (e.g. cache warming, DB pool init)
# Gives the container up to (30 * 5s) = 150 seconds to complete cold start.
# Liveness and readiness probes remain inactive until this succeeds.
startupProbe:
httpGet:
path: /livez
port: http
scheme: HTTP
initialDelaySeconds: 3
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 30
# 2. LIVENESS PROBE: Tests in-process runtime health ONLY.
# Destroys and restarts the container if the process freezes or deadlocks.
# NEVER touches external databases or caches.
livenessProbe:
httpGet:
path: /livez
port: http
scheme: HTTP
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
successThreshold: 1
# 3. READINESS PROBE: Controls whether this pod receives customer traffic.
# Evaluates local connection pool capacity and essential dependencies.
# Failure removes pod from EndpointSlice without terminating the container.
readinessProbe:
httpGet:
path: /readyz
port: http
scheme: HTTP
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
successThreshold: 1
# 4. GRACEFUL DRAIN LIFECYCLE HOOK:
# Solves the race condition between EndpointSlice propagation and SIGTERM.
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
Why the preStop Sleep Hook is Mandatory
When a pod is deleted (e.g., during a rolling deployment or autoscaling downscale):
- The Kubernetes API server simultaneously marks the pod as
Terminatingand informs the EndpointSlice controller. - The EndpointSlice controller removes the pod IP and updates the cluster's worker nodes.
- In parallel,
kubeletimmediately executes the container's termination sequence, dispatchingSIGTERM.
Because network propagation of the updated EndpointSlice to all kube-proxy iptables/IPVS instances and external ingress controllers takes several seconds (typically 2–8 seconds in busy clusters), ingress routers will continue forwarding new customer requests to the pod after the application has already begun shutting down.
The preStop: sleep 15 hook pauses the shutdown sequence for 15 seconds before SIGTERM is delivered. During these 15 seconds, the pod is removed from all load balancer routing tables. By the time SIGTERM actually reaches the application process, no new traffic is arriving, and existing in-flight connections can be drained cleanly without generating HTTP 502 errors.
7. Production-Grade TypeScript Implementation: HealthCheckManager
The following self-contained TypeScript module provides a production-grade health checking engine. It separates liveness from readiness, enforces per-component execution timeouts using AbortSignal.timeout, caches results in memory to eliminate thundering herd query floods, and outputs RFC-compliant application/health+json responses.
/**
* Types conforming to the IETF RFC draft for HTTP API Health Checks
* (draft-inadarei-api-health-check-06)
*/
export type HealthStatus = 'pass' | 'warn' | 'fail';
export interface ComponentCheckResult {
componentType: string;
status: HealthStatus;
observedValue?: string | number;
observedUnit?: string;
output?: string;
time: string;
}
export interface HealthCheckResponse {
status: HealthStatus;
version?: string;
releaseId?: string;
checks: Record<string, ComponentCheckResult[]>;
}
export interface CheckExecutionOutput {
status: HealthStatus;
observedValue?: string | number;
observedUnit?: string;
output?: string;
}
export interface ComponentRegistration {
name: string;
componentType: string;
timeoutMs: number;
ttlMs: number;
isLiveness: boolean; // true = evaluated in liveness; false = readiness only
check: (signal: AbortSignal) => Promise<CheckExecutionOutput>;
}
interface CachedResult {
result: ComponentCheckResult;
expiresAt: number;
}
export interface HealthCheckExecutionResult {
statusCode: number;
headers: Record<string, string>;
body: HealthCheckResponse;
}
export class HealthCheckManager {
private readonly registrations: Map<string, ComponentRegistration> = new Map();
private readonly cache: Map<string, CachedResult> = new Map();
private readonly releaseId?: string;
private readonly version?: string;
constructor(options?: { releaseId?: string; version?: string }) {
this.releaseId = options?.releaseId;
this.version = options?.version;
}
/**
* Registers a health check component.
*/
public register(registration: ComponentRegistration): void {
this.registrations.set(registration.name, registration);
}
/**
* Executes liveness checks.
* Tests only internal process viability (e.g. event loop responsiveness).
* Strictly avoids downstream network dependencies.
*/
public async executeLiveness(): Promise<HealthCheckExecutionResult> {
return this.runChecks((reg) => reg.isLiveness);
}
/**
* Executes readiness checks.
* Tests external dependencies and capacity to process customer traffic.
*/
public async executeReadiness(): Promise<HealthCheckExecutionResult> {
return this.runChecks((reg) => !reg.isLiveness);
}
/**
* Internal execution pipeline with per-check timeouts and TTL caching.
*/
private async runChecks(
filter: (reg: ComponentRegistration) => boolean
): Promise<HealthCheckExecutionResult> {
const checksToRun = Array.from(this.registrations.values()).filter(filter);
const checksOutput: Record<string, ComponentCheckResult[]> = {};
const now = Date.now();
const results = await Promise.all(
checksToRun.map(async (reg) => {
// 1. Check in-memory TTL cache to prevent probe-induced thundering herds
const cached = this.cache.get(reg.name);
if (cached && cached.expiresAt > now) {
return { name: reg.name, result: cached.result };
}
// 2. Enforce aggressive per-component timeout using AbortSignal.timeout
const signal = AbortSignal.timeout(reg.timeoutMs);
try {
const rawResult = await reg.check(signal);
const fullResult: ComponentCheckResult = {
componentType: reg.componentType,
status: rawResult.status,
observedValue: rawResult.observedValue,
observedUnit: rawResult.observedUnit,
output: rawResult.output,
time: new Date().toISOString(),
};
// Cache healthy or warning results for the configured TTL window
this.cache.set(reg.name, {
result: fullResult,
expiresAt: Date.now() + reg.ttlMs,
});
return { name: reg.name, result: fullResult };
} catch (error) {
const isTimeout = signal.aborted;
const fullResult: ComponentCheckResult = {
componentType: reg.componentType,
status: 'fail',
output: isTimeout
? `Check timed out after ${reg.timeoutMs}ms`
: error instanceof Error
? error.message
: String(error),
time: new Date().toISOString(),
};
// Cache failures briefly (max 1000ms) to throttle rapid retries under load
this.cache.set(reg.name, {
result: fullResult,
expiresAt: Date.now() + Math.min(reg.ttlMs, 1000),
});
return { name: reg.name, result: fullResult };
}
})
);
let overallStatus: HealthStatus = 'pass';
for (const { name, result } of results) {
checksOutput[name] = [result];
if (result.status === 'fail') {
overallStatus = 'fail';
} else if (result.status === 'warn' && overallStatus !== 'fail') {
overallStatus = 'warn';
}
}
// Map RFC status to HTTP status code:
// pass -> 200 OK
// warn -> 200 OK (service degraded but operational; load balancer retains node)
// fail -> 503 Service Unavailable (load balancer evicts node from routing)
const statusCode = overallStatus === 'fail' ? 503 : 200;
return {
statusCode,
headers: {
'Content-Type': 'application/health+json',
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
body: {
status: overallStatus,
version: this.version,
releaseId: this.releaseId,
checks: checksOutput,
},
};
}
}
7.1 Integrating the Manager with a Production Node.js Service
Here is how to register checks, wire up event loop lag monitoring, and mount the endpoints onto an HTTP server:
import { createServer } from 'node:http';
const healthManager = new HealthCheckManager({
version: '1.4.2',
releaseId: 'prod-commit-a4f912c',
});
// 1. Register Liveness Check: In-process event loop responsiveness
healthManager.register({
name: 'process:event-loop',
componentType: 'system',
timeoutMs: 250,
ttlMs: 2000,
isLiveness: true,
check: async () => {
const start = performance.now();
await new Promise((resolve) => setImmediate(resolve));
const lagMs = performance.now() - start;
return {
status: lagMs > 100 ? 'warn' : 'pass',
observedValue: Math.round(lagMs),
observedUnit: 'ms',
output: lagMs > 100 ? 'Elevated event loop lag' : 'Event loop responsive',
};
},
});
// 2. Register Readiness Check: Database connection pool availability
healthManager.register({
name: 'datastore:primary-pg',
componentType: 'datastore',
timeoutMs: 500,
ttlMs: 2000, // 2-second in-memory cache shields DB from high-frequency probe floods
isLiveness: false,
check: async (signal) => {
if (signal.aborted) {
throw new Error('Aborted prior to pool inspection');
}
// Inspect internal pool statistics directly without acquiring/blocking on a physical connection
const poolStats = { total: 20, idle: 14, waiting: 0 };
if (poolStats.waiting > 10) {
return {
status: 'fail',
observedValue: poolStats.waiting,
observedUnit: 'waiting-callers',
output: 'Connection pool starved; waiting queue exceeds safety ceiling',
};
}
return {
status: 'pass',
observedValue: poolStats.idle,
observedUnit: 'idle-connections',
output: 'Database connection pool healthy',
};
},
});
// 3. Mount HTTP Handlers
const server = createServer(async (req, res) => {
if (req.url === '/livez') {
const { statusCode, headers, body } = await healthManager.executeLiveness();
res.writeHead(statusCode, headers);
res.end(JSON.stringify(body, null, 2));
return;
}
if (req.url === '/readyz') {
const { statusCode, headers, body } = await healthManager.executeReadiness();
res.writeHead(statusCode, headers);
res.end(JSON.stringify(body, null, 2));
return;
}
res.writeHead(404);
res.end('Not Found');
});
server.listen(8080);
8. Actionable Production Verification Checklist
Before promoting health check configurations to production, verify each item on this operational checklist:
- [ ] Liveness Isolation: The
/livezhandler does not execute queries against databases, distributed caches, or external APIs under any circumstances. - [ ] Timeout Budgeting: Every component check within
/readyzhas an explicittimeoutMsstrictly lower than the orchestrator'stimeoutSeconds(e.g., 500 ms component timeout vs. 2,000 ms probe timeout). - [ ] Probe Flapping Smoothing:
failureThresholdis configured to 3 or higher on both liveness and readiness probes to absorb transient GC pauses and network blips. - [ ] Thundering Herd Shielding: Health check responses utilize short in-memory caching (e.g., 1–3 seconds TTL) to prevent hundred-node horizontal fleets from overwhelming databases with polling traffic.
- [ ] Non-Blocking Pool Inspection: Readiness checks verify connection pool capacity using internal driver metrics rather than checking out physical connections from a shared pool.
- [ ] Startup Probe Coverage: Applications with cold-start initialization exceeding 10 seconds define a
startupProbewith adequate failure threshold budgets, keeping runtime liveness probes tight. - [ ] Graceful Shutdown Synchronization: Container specs define a
preStophook (e.g.,sleep 15) and adequateterminationGracePeriodSecondsto ensureEndpointSlicerouting eviction propagates beforeSIGTERMhalts the process. - [ ] HTTP Protocol Compliance: Failed checks return HTTP 503 Service Unavailable, degraded checks return HTTP 200 OK with
"warn", and responses enforceCache-Control: no-cache, no-store, must-revalidate. - [ ] Diagnostic Security: Deep administrative status routes (
/healthz/status) requiring detailed connection pool or infrastructure metrics are protected by authentication or internal loopback bindings. - [ ] Failure Observability: Probe failures increment Prometheus counters and emit structured logs detailing which sub-component caused the failure.
Conclusion: Health Checks as Resilient Control Loops
Health checks are the nervous system of modern distributed infrastructure. When built thoughtfully, they provide orchestrators and load balancers with the precise intelligence needed to heal crashed nodes and route around transient degradation.
By enforcing strict separation between process liveness and traffic readiness, protecting downstream datastores with in-memory TTL caching, smoothing probe failures across multiple samples, and adhering to standard RFC formats, teams can eliminate the risk of cascading reboot storms and maintain high availability even when underlying dependencies falter.


