CodeKits.shopNew developer offer

Build liveness and readiness checks for Node

Build a local Node health-check demo with bounded readiness probes, failure tests and graceful draining, without leaking database details.

By Jonathan · 5 min read

Published

Separate liveness and readiness checks with a bounded dependency probe
In this article

A process can be alive while its database is unavailable. A database can respond while an application is about to shut down. Treating both situations as a single always-green health page hides the failures a deployment check should catch.

This dependency-free Node laboratory distinguishes liveness, whether the HTTP process can respond, from readiness, whether the instance should receive useful work. It is a companion to the VPS learning path, not a complete monitoring service.

Run the included example

Use a supported Node release; the example is designed for Node 22 or 24. Check the Node release schedule when choosing production versions. Extract the download and run from its directory:

Local terminal: tests and demonstration
node --test health.test.mjs
node demo.mjs

Leave the demo running and open a second terminal:

Local terminal: exercise both endpoints
curl --connect-timeout 2 --max-time 3 -i http://127.0.0.1:3400/live
curl --connect-timeout 2 --max-time 3 -i http://127.0.0.1:3400/ready

Both return 200 with {"ok":true}. This is expected because the demonstration dependency always succeeds. It has not contacted MySQL. Do not deploy that stub and describe it as a database health check.

Project file tree
node-liveness-readiness-checks/
├── demo.mjs
├── health.mjs
├── health.test.mjs
└── README.md

Stop the demo with Ctrl-C. It binds only to loopback; no firewall changes or remote accounts are required.

Give each endpoint a small contract

Endpoint Healthy result Failure meaning
GET /live 200 The HTTP process cannot answer when unavailable
GET /ready 200 503 means dependency failure, timeout or draining
Other path 404 Not part of this teaching server
Unsupported method 405 GET and HEAD only

Responses use Cache-Control: no-store so an intermediary should not replay a stale success. They do not expose database hostnames, stack traces or credentials. Restrict or rate-limit health ingress in a real deployment even if the response body is minimal.

Readiness failure should normally block promotion or remove an instance from traffic. It should not automatically restart every instance during a shared database outage: that can create a reconnect storm.

Bound probes without accumulating abandoned work

The helper takes a probe(signal) function that returns true only when its dependency condition is met. A deadline produces a 503 and aborts the signal. Simultaneous readiness requests share one pending probe.

demo.mjs — complete synthetic startup
import { createHealthServer } from './health.mjs';
 
const { server } = createHealthServer({
  probe: async () => true
});
server.listen(3400, '127.0.0.1');

The download includes shutdown handling too. Replace the always-true stub with a real dependency adapter whose connection acquisition, query and cleanup are bounded. Passing an AbortSignal does not make a database driver honour it. Use the driver's supported timeout/cancellation facilities and release or destroy connections correctly.

A Promise.race deadline can return while the original query continues. Repeating it on every health request can leak work. The helper therefore keeps an uncooperative timed-out probe single-flight until it really settles; subsequent requests receive the failed result. Alert on sustained failure and repair the adapter instead of launching unbounded background operations.

For MySQL, a cheap query can prove connectivity but not that the required migration exists. If readiness depends on a schema version, inspect a small migration marker rather than scanning application tables. Never perform migrations inside a health request.

Drain before stopping

The helper checks its draining flag both before and after awaiting the probe. If shutdown starts during a slow successful check, that request must not return a misleading ready result.

The full demo marks draining, calls server.close and gives outstanding HTTP requests up to five seconds before force-closing remaining connections. Repeated stop signals do not create competing shutdown handlers. See Node's HTTP shutdown API for the lifecycle details.

Where a load balancer polls readiness, first mark draining, allow its documented propagation time, then stop accepting connections. A single-instance VPS cannot guarantee zero downtime by adding a flag. WebSockets, long uploads, workers and transactions each need a separate bounded shutdown policy. Close database pools only after the work using them has finished or been cancelled safely.

Test failures, not only the green response

The included tests exercise successful probes, dependency exceptions, unsupported routes/methods, empty HEAD responses, no-store headers, timeout coalescing and draining during a successful probe.

The timeout test deliberately uses an uncooperative Promise. It verifies that eight concurrent requests do not start eight dependency operations. The exception test uses a private-looking error and confirms the public response remains only {"ok":false}.

Four automated local test groups pass. They validate the helper, not a real database, proxy or load balancer. Before production, test dependency loss and recovery in staging, confirm your actual driver releases resources, and verify your supervisor gives shutdown enough time. Run curl acceptance checks through HTTPS as well as the loopback listener.

Put the result in a release decision

Record the release identifier, health outcome and one representative application transaction. Do not promote solely because the process exists. Equally, keep detailed diagnostics in protected logs rather than making them public for convenience.

A useful release process stops at an unhealthy candidate, keeps the current version available and preserves enough evidence to investigate. The staged-release rollback guide shows where that gate belongs.