How to test a health check endpoint
Send one GET and read the status code, not the body: curl -sS -o /dev/null -w 'code=%{http_code} time=%{time_total}s\n' http://127.0.0.1:8763/healthz printed code=200 time=0.008486s. Then break a dependency and repeat the request. A health endpoint worth having answers 503 within the same second, and a probe reading only exit codes cannot tell those two apart.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
A health endpoint is the contract between your service and everything that decides whether to send it traffic: a load balancer pool, a container orchestrator, a deployment gate, an uptime monitor. Test it on staging sign-off and again after any change to the handler, because nothing else in the system fails loudly when it is wrong.
The failure it prevents is a pool that never drains. A service whose database is gone keeps answering 200 on /healthz, the load balancer keeps routing to it, and every request it receives returns an error to a customer. The health endpoint was the one component that could have removed the instance, and it reported success.
Prerequisites
- Node 18 or later. The fixture below uses only the
httpmodule. - curl 7.75 or later.
--failand-w '%{http_code}'are both used. - A free port.
netstat -ano | grep 8763prints nothing when 8763 is free. - MDN on 503 for what the status commits the service to.
Save this as health-server.js. It answers /healthz and carries a control route that flips the reported health, so both answers come from one process.
// node health-server.js listens on http://127.0.0.1:8763
const http = require('http');
const state = { healthy: true };
const send = (res, code, body) => {
res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' });
res.end(JSON.stringify(body));
};
http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1');
if (url.pathname === '/control') { // test hook, never shipped
state.healthy = url.searchParams.get('healthy') !== 'false';
return send(res, 200, state);
}
if (url.pathname === '/healthz') {
return state.healthy
? send(res, 200, { status: 'ok', version: '1.4.2' })
: send(res, 503, { status: 'unhealthy', reason: 'order-db unreachable' });
}
send(res, 404, { status: 'not found' });
}).listen(8763, '127.0.0.1', () => console.log('health fixture on 127.0.0.1:8763'));
Start it, and read the Windows PID out of netstat for the last step.
node health-server.js & netstat -ano | grep 8763
Steps
- Step 1.
Send one request and read the three numbers that matter, without the body in the way.
curl -sS -o /dev/null -w 'code=%{http_code} time=%{time_total}s size=%{size_download}\n' http://127.0.0.1:8763/healthzcode=200 time=0.008486s size=33A 33 byte body answered in 8 ms. Latency on a loopback address has no network in it, so treat this number as the handler's own cost and nothing more.
- Step 2.
Read the headers and the body once, to record the shape the probe will be matched against.
curl -sS -i http://127.0.0.1:8763/healthzHTTP/1.1 200 OK content-type: application/json cache-control: no-store Date: Fri, 11 Sep 2026 22:54:19 GMT Connection: keep-alive Keep-Alive: timeout=5 Transfer-Encoding: chunked {"status":"ok","version":"1.4.2"}cache-control: no-storeis the line to look for. Without it a proxy may answer on the service's behalf and the probe never reaches the process. - Step 3.
Break the reported health through the control route, so the next request has something to fail on.
curl -sS "http://127.0.0.1:8763/control?healthy=false"{"healthy":false}In a real service this is a stopped database, a full disk or a failed migration. The control route exists so the page can produce the state on demand.
- Step 4.
Repeat the request from step 2 against the unhealthy service.
curl -sS -i http://127.0.0.1:8763/healthzHTTP/1.1 503 Service Unavailable content-type: application/json cache-control: no-store Date: Fri, 11 Sep 2026 22:54:27 GMT Connection: keep-alive Keep-Alive: timeout=5 Transfer-Encoding: chunked {"status":"unhealthy","reason":"order-db unreachable"}Same URL, same handler, a different status line. That is the only part a load balancer reads.
- Step 5.
Run the endpoint the way a smoke test usually does, with plain curl and a shell conjunction.
curl -s -o /dev/null http://127.0.0.1:8763/healthz && echo "smoke test passed"smoke test passedThe service said 503 and the test said passed. curl returns 0 for any response it managed to receive, whatever the status code was.
- Step 6.
Add
--failand run the identical request.curl -sf -o /dev/null http://127.0.0.1:8763/healthz && echo "smoke test passed" || echo "smoke test failed, curl exit $?"smoke test failed, curl exit 22Exit 22 is curl's code for an HTTP response above 400 under
--fail. This is the one flag that makes a health check usable in a pipeline. - Step 7.
Stop the process with the PID from
netstat, then send the same request to a port with no listener.curl -sS -o /dev/null -w 'code=%{http_code}\n' http://127.0.0.1:8763/healthz; echo "exit=$?"curl: (7) Failed to connect to 127.0.0.1:8763 after 2026 ms: Could not connect to server code=000 exit=7There is no status code, so
%{http_code}prints000, and the refusal took 2026 ms on this machine rather than returning at once. A check that storeshttp_codeand alerts on 5xx sees nothing here.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| code=200 and a body naming the build | The handler ran and claims health | Record the body shape and assert on it in the test, not on the words. |
| code=503 | The service removed itself from the pool | Confirm the pool drained, then find the dependency named in the body. |
| code=000, curl exit 7 | Nothing is listening: the process is gone | Different fault from 503. Both are down, only one can report a reason. |
| curl exit 28 | The handler accepted the connection and never answered | See Health check timeout. |
| code=200 with unhealthy in the body | Status code and body disagree | See Deep health check. |
Common mistakes
What to check next
- Readiness probe vs liveness probe: why one endpoint cannot answer both questions.
- Deep health check: what happens when the handler starts touching dependencies.
- Health check timeout: the failure mode where the endpoint answers neither 200 nor 503.
- How to check API endpoint: the same reading applied to a business endpoint rather than a probe.
FAQ
What is the use of a /health endpoint?
It gives one machine-readable answer to one question: should traffic go to this instance right now. Load balancers, orchestrators and deploy gates poll it and act on the status code. Nothing else in the system offers to take an instance out of rotation on its own.
Is there a standard for health check endpoints?
No RFC defines one. The paths /healthz, /health and /status are all in common use. Treat the path, the status code rule and the body shape as a contract you write down and test, because the platform polling it will only read the status code.
How do I check a health endpoint with curl?
curl -sf -o /dev/null -w '%{http_code}' <url>. The -f makes a 5xx a non-zero exit, -o /dev/null keeps the body out of the output, and -w prints the status code that the pool decision is made on.
What should a healthz endpoint return?
200 when the instance can serve, 503 when it cannot, and a small JSON body naming the build and the failing dependency. Keep it under a second and never let it require authentication that the probe cannot supply.
Does a 200 mean the service works?
It means the handler ran and returned 200. Whether it looked at anything is a separate question, answered by reading the body and by the deep check page. Status code and body can disagree, and monitors usually read only one of them.
Verified
Verified by Maks Vernycurl 8.21.0node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
- Checker: health-endpoint GET a health URL, status code, response time, JSON body shape, cache headers that would mask failures
- All logging and observability checks
basic8 minpublished updated Maks Verny