Readiness probe vs liveness probe
Read both endpoints in one request: curl -sS -o /dev/null -o /dev/null -w '%{http_code} %{url}\n' http://127.0.0.1:8763/livez http://127.0.0.1:8763/readyz. During warm-up that printed 200 for liveness and 503 for readiness. Then take the database away and confirm liveness stays 200, because restarting a process does not bring a dependency back.
Why check this
The two probes answer different questions. Liveness asks whether this process is still working and a failure restarts the container. Readiness asks whether this instance can serve a request right now and a failure only removes it from the pool. Check the pair when probes are added to a deployment manifest, and again whenever someone changes what a probe handler touches.
The failure it prevents is an outage amplifier. If the liveness handler checks the database, a database that blinks for 30 seconds restarts every instance of a service that was working. The restarts then land together on the recovering database.
Prerequisites
- Node 18 or later. The fixture uses the
httpmodule and nothing else. - curl 7.75 or later, for
-w '%{http_code}'and several URLs in one call. - A free port.
netstat -ano | grep 8763prints nothing when 8763 is free. - Kubernetes probe documentation for the fields a platform applies around the endpoint.
- There is no cluster on this machine. Step 7 runs the arithmetic a kubelet applies to a probe, not a kubelet.
Save this as probe-server.js. It exposes three endpoints: a liveness probe that touches nothing, a liveness probe written the common wrong way, and a readiness probe.
// node probe-server.js listens on http://127.0.0.1:8763
const http = require('http');
const state = { db: 'up', ready: false };
const send = (res, code, body) => {
res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' });
res.end(JSON.stringify(body));
};
const db = () => { if (state.db !== 'up') throw new Error('order-db: connect ECONNREFUSED'); };
http.createServer((req, res) => {
const u = new URL(req.url, 'http://127.0.0.1');
const p = u.searchParams;
switch (u.pathname) {
case '/control': // test hook, never shipped
if (p.has('db')) state.db = p.get('db');
if (p.has('ready')) state.ready = p.get('ready') === 'true';
return send(res, 200, state);
case '/livez': // liveness: is this process still running
return send(res, 200, { status: 'alive', pid: process.pid });
case '/livez-deep': // the anti-pattern: liveness touching the database
try { db(); return send(res, 200, { status: 'alive' }); }
catch (e) { return send(res, 503, { status: 'dead', error: e.message }); }
case '/readyz': { // readiness: can it serve traffic right now
let dbOk = true;
try { db(); } catch { dbOk = false; }
const ok = state.ready && dbOk;
return send(res, ok ? 200 : 503, {
status: ok ? 'ready' : 'not_ready',
checks: { warmup: state.ready ? 'ok' : 'starting', database: dbOk ? 'ok' : 'fail' }
});
}
default: return send(res, 404, { status: 'not found' });
}
}).listen(8763, '127.0.0.1', () => console.log('probe fixture on 127.0.0.1:8763 pid ' + process.pid));
Save this as probe-loop.mjs. It polls one URL on a period and applies a failure threshold, which is all a liveness probe does.
// node probe-loop.mjs <url> <periodSeconds> <failureThreshold>
// The arithmetic a kubelet applies to a liveness probe. This is not a cluster.
const [url, period = '1', threshold = '3'] = process.argv.slice(2);
let fails = 0;
for (let i = 1; i <= 6; i += 1) {
let code = 'ERR';
try {
code = String((await fetch(url, { signal: AbortSignal.timeout(1000) })).status);
} catch { code = 'ERR'; }
fails = code === '200' ? 0 : fails + 1;
const verdict = fails >= Number(threshold) ? 'RESTART CONTAINER' : `failure ${fails}/${threshold}`;
console.log(`t=${i}s ${code} ${code === '200' ? 'ok' : verdict}`);
if (fails >= Number(threshold)) break;
await new Promise((r) => setTimeout(r, Number(period) * 1000));
}
Start the fixture and keep the printed PID for the last step.
node probe-server.js & netstat -ano | grep 8763
Steps
- Step 1.
Read both probes while the process is still warming up.
curl -sS -o /dev/null -o /dev/null -w '%{http_code} %{url}\n' http://127.0.0.1:8763/livez http://127.0.0.1:8763/readyz200 http://127.0.0.1:8763/livez 503 http://127.0.0.1:8763/readyzAlive and not taking traffic. One
-ocovers one URL, so two URLs need two. - Step 2.
Read the readiness body to find out which condition is holding it back.
curl -sS http://127.0.0.1:8763/readyz{"status":"not_ready","checks":{"warmup":"starting","database":"ok"}}The
checksobject names the reason. A readiness endpoint that returns a bare 503 tells an operator nothing during an incident. - Step 3.
Mark the warm-up complete, the way a service does once its caches are filled.
curl -sS "http://127.0.0.1:8763/control?ready=true"{"db":"up","ready":true} - Step 4.
Read both probes again.
curl -sS -o /dev/null -o /dev/null -w '%{http_code} %{url}\n' http://127.0.0.1:8763/livez http://127.0.0.1:8763/readyz200 http://127.0.0.1:8763/livez 200 http://127.0.0.1:8763/readyzLiveness did not move. Only readiness changed.
- Step 5.
Take the database away while the process keeps running.
curl -sS "http://127.0.0.1:8763/control?db=down"{"db":"down","ready":true} - Step 6.
Read all three endpoints against that one fault.
curl -sS -o /dev/null -o /dev/null -o /dev/null -w '%{http_code} %{url}\n' http://127.0.0.1:8763/livez http://127.0.0.1:8763/livez-deep http://127.0.0.1:8763/readyz200 http://127.0.0.1:8763/livez 503 http://127.0.0.1:8763/livez-deep 503 http://127.0.0.1:8763/readyzOne incident, three verdicts. The middle line is a liveness handler that reaches into the database, and it now reports a healthy process as dead.
- Step 7.
Apply a liveness probe's arithmetic to that endpoint: one poll per second, three consecutive failures.
node probe-loop.mjs http://127.0.0.1:8763/livez-deep 1 3t=1s 503 failure 1/3 t=2s 503 failure 2/3 t=3s 503 RESTART CONTAINERThree seconds of a database fault and the platform kills a process that was answering correctly.
- Step 8.
Run the same loop against the liveness endpoint that touches nothing.
node probe-loop.mjs http://127.0.0.1:8763/livez 1 3t=1s 200 ok t=2s 200 ok t=3s 200 ok t=4s 200 ok t=5s 200 ok t=6s 200 okSame outage, no restart. The instance stays out of the pool through readiness and comes back on its own.
- Step 9.
Bring the database back, with the process untouched.
curl -sS "http://127.0.0.1:8763/control?db=up"{"db":"up","ready":true} - Step 10.
Confirm readiness recovers on its own.
curl -sS -o /dev/null -o /dev/null -w '%{http_code} %{url}\n' http://127.0.0.1:8763/livez-deep http://127.0.0.1:8763/readyz200 http://127.0.0.1:8763/livez-deep 200 http://127.0.0.1:8763/readyzA handler that latches its failure never returns to 200, and a short fault becomes a manual recovery.
- Step 11.
Stop the process with the PID from
netstat, start it again, and read both probes at once. This is the state the restart in step 7 would have produced.curl -sS -o /dev/null -o /dev/null -w '%{http_code} %{url}\n' http://127.0.0.1:8763/livez http://127.0.0.1:8763/readyz200 http://127.0.0.1:8763/livez 503 http://127.0.0.1:8763/readyzThe restarted process is alive and back in warm-up. A restart cannot end a dependency outage, and it adds this window to it.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Liveness 200, readiness 503 | The process runs and is not serving | Read the checks object. This is correct during warm-up and during a dependency fault. |
| Both 200 | In the pool | Nothing. Record the latency of each handler. |
| Liveness 503 | The process itself is broken | A restart is the right response only here: a deadlock, a wedged loop, a leaked handle. |
| Liveness 503 at the same moment as readiness | The liveness handler is checking a dependency | Move that check to readiness. Step 7 shows what the platform does with it. |
| Readiness 200 while a dependency is down | The handler is not checking anything | See Deep health check. |
Thresholds
Common mistakes
What to check next
- How to test a health check endpoint: the single endpoint case, and the curl exit codes both probes are read with.
- Deep health check: how far a readiness handler should reach, and what it must not do.
- Health check timeout: the probe result that is neither 200 nor 503.
- How to test graceful degradation: what the service should still serve while readiness says no.
FAQ
What is the difference between a liveness and a readiness probe?
Liveness asks whether the process must be restarted. Readiness asks whether traffic may be sent to it. A liveness failure is destructive and a readiness failure is not, so anything outside the process belongs in readiness.
How do I check that a readiness probe works?
Put the instance into each state it claims to detect and read the status code. Warm-up, dependency down, dependency back. Steps 1, 6 and 10 are those three states, and a handler that misses any of them is not checking what it says.
How do I check a liveness probe is working?
Confirm it stays 200 through a dependency outage, then confirm it fails when the process is genuinely stuck. If you cannot make it fail without breaking something outside the process, it is a readiness probe with the wrong name.
What should a readyz endpoint return?
200 when the instance can serve, 503 when it cannot, and a body naming each condition, as in step 2. Keep it fast: the platform polls it every few seconds on every instance.
Can one endpoint serve both probes?
Only when it checks nothing but the process, which makes it a liveness endpoint doing no readiness work. Step 7 shows the cost.
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
intermediate12 minpublished updated Maks Verny