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

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

  1. 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/readyz
    
    200  http://127.0.0.1:8763/livez
    503  http://127.0.0.1:8763/readyz

    Alive and not taking traffic. One -o covers one URL, so two URLs need two.

  2. 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 checks object names the reason. A readiness endpoint that returns a bare 503 tells an operator nothing during an incident.

  3. 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}
  4. 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/readyz
    
    200  http://127.0.0.1:8763/livez
    200  http://127.0.0.1:8763/readyz

    Liveness did not move. Only readiness changed.

  5. 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}
  6. 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/readyz
    
    200  http://127.0.0.1:8763/livez
    503  http://127.0.0.1:8763/livez-deep
    503  http://127.0.0.1:8763/readyz

    One incident, three verdicts. The middle line is a liveness handler that reaches into the database, and it now reports a healthy process as dead.

  7. 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 3
    
    t=1s  503  failure 1/3
    t=2s  503  failure 2/3
    t=3s  503  RESTART CONTAINER

    Three seconds of a database fault and the platform kills a process that was answering correctly.

  8. 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 3
    
    t=1s  200  ok
    t=2s  200  ok
    t=3s  200  ok
    t=4s  200  ok
    t=5s  200  ok
    t=6s  200  ok

    Same outage, no restart. The instance stays out of the pool through readiness and comes back on its own.

  9. 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}
  10. 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/readyz
    
    200  http://127.0.0.1:8763/livez-deep
    200  http://127.0.0.1:8763/readyz

    A handler that latches its failure never returns to 200, and a short fault becomes a manual recovery.

  11. 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/readyz
    
    200  http://127.0.0.1:8763/livez
    503  http://127.0.0.1:8763/readyz

    The 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

3 consecutive failures at the default 10 s probe period restarts the container roughly 30 s into any dependency fault Source: kubernetes.io, Liveness, Readiness and Startup Probes: 'The default for periodSeconds is 10s', read 2026-09-12

Common mistakes

Sign: One handler is wired to both probes because the manifest allowed it.Cause: The same status then answers two different questions. When a dependency fails, readiness drops, which is correct, and liveness drops with it, which restarts a working process. Step 7 reaches RESTART CONTAINER after three seconds of a fault the process did not cause.
Sign: A restart cleared the incident, so the liveness probe looks justified.Cause: A restart clears process state, and nothing else. Step 11 shows the restarted process reporting alive and not_ready while the database is still gone, so the restart added a warm-up window to an outage it could not end.
Sign: Readiness never returns to 200 after the dependency recovers.Cause: A handler that caches its verdict, or sets a failed flag once, stays down until someone restarts it. That turns a 30 second blip into a manual recovery. Step 10 is the check: flip the dependency back and watch 200 return without touching the process.
Sign: Readiness is 200 from the first millisecond of the process.Cause: Caches, connection pools and JIT warm-up are not finished when the port opens. Traffic arriving in that window sees timeouts on a green instance. Step 1 shows what the correct answer looks like during warm-up: alive, not ready.

What to check next

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.

intermediate12 minpublished updated Maks Verny