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:
node --test health.test.mjs
node demo.mjsLeave the demo running and open a second terminal:
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/readyBoth 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.
node-liveness-readiness-checks/
├── demo.mjs
├── health.mjs
├── health.test.mjs
└── README.mdStop 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.
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.
