A pod can be Running and still be useless. The container process is alive, the port is open, and every request that hits it times out anyway, because the database connection pool hasn't finished warming up, or a downstream dependency is unreachable, or the app is stuck in a loop it will never recover from on its own.
Kubernetes has two checks for exactly this, livenessProbe and readinessProbe. They sit next to each other in the same YAML block, take the same shape, and do almost nothing alike.
What liveness actually does
A liveness probe answers one question: is this process still working, or should it be killed and restarted.
If the probe fails enough times in a row, Kubernetes kills the container and starts a fresh one. That's the entire mechanism. It exists for the case where your process is technically running but permanently stuck, a deadlock, a hung thread, a memory leak that's wedged the event loop. Restarting is the only fix, and liveness is what triggers it automatically instead of you finding out from an alert three hours later.
What it does not do: protect you from slow startup, temporary unavailability, or a dependency being down. If your liveness check fails because the database is briefly unreachable, Kubernetes restarts a perfectly healthy process, which does nothing to fix the database and just adds a few seconds of extra downtime while the container comes back up.
What readiness actually does
A readiness probe answers a different question: should this pod currently receive traffic.
If it fails, the pod is pulled out of the Service's list of endpoints. Nothing gets restarted. The container keeps running, Kubernetes just stops routing requests to it until the probe passes again. This is what you want during startup, while dependencies are still initializing, and during temporary trouble, when a downstream service is degraded and you'd rather drain traffic away than serve errors.
Readiness is safe to make strict. Liveness is not.
The mistake that causes the most damage
Pointing both probes at the same endpoint, or writing a liveness check that depends on downstream services.
// dangerous as a liveness check
app.get('/healthz', async (req, res) => {
await db.query('SELECT 1');
await redis.ping();
res.sendStatus(200);
});
The moment the database is slow or Redis hiccups, every pod running this as its liveness check starts failing and getting restarted, all at once, across the whole deployment. You just turned a downstream blip into a full outage of your own service, and a restart storm on top of it, since the new containers boot up, immediately fail the same check because the dependency is still down, and get killed again.
What each check should actually look like
Liveness should only answer "is the process itself alive," nothing more.
// liveness: just prove the event loop is responsive
app.get('/healthz', (req, res) => {
res.sendStatus(200);
});
Readiness should check the things that actually determine whether this pod can serve a request correctly.
// readiness: are the things we depend on actually up
app.get('/ready', async (req, res) => {
try {
await db.query('SELECT 1');
res.sendStatus(200);
} catch {
res.sendStatus(503);
}
});
And the pod spec pointing at each:
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
Tuning that actually matters
initialDelaySeconds on liveness should be longer than your worst-case startup time. If your app can take 20 seconds to boot under load, and liveness starts checking at 10 seconds with a low failure threshold, you'll get restart loops on your slowest starts, which makes the slow start even slower.
failureThreshold and periodSeconds together define how long a probe has to fail before something happens. Three failures at ten second intervals gives you thirty seconds of grace. Too short and normal jitter triggers unnecessary restarts or traffic pulls. Too long and a genuinely stuck pod keeps eating requests for a while before anyone notices.
Never let liveness depend on anything outside the process. If it can fail because of a network call, a database, another service, it will eventually cause a restart storm during exactly the incident where you can least afford one.
Readiness can and should depend on those things. That's the entire point of having it separate. Use it to pull a pod out of rotation the moment it can't correctly serve a request, and let it rejoin automatically the moment it can again, with no restart, no lost in-flight work on the other healthy pods.
Takeaway
Liveness restarts a stuck process. Readiness controls whether a pod gets traffic. Keep liveness dumb and self-contained so it only fires when the process itself is actually broken, and keep readiness honest about your real dependencies so Kubernetes can route around trouble instead of amplifying it.
Top comments (0)