Kubernetes liveness, readiness, startup probes done right
Stop routing traffic to pods that aren't ready and restarts that make outages worse — the three probes explained.
The kubelet knows a container's process is running. It does not know whether the process can serve a request, whether it is deadlocked, or whether it is thirty seconds into a ninety-second cache warm-up. Probes answer those questions, and each of the three answers has a different consequence, which is the only thing that matters when choosing which check goes where.
Three probes, three consequences
| Probe | Question | When it fails | What belongs in it |
|---|---|---|---|
| readiness | can this Pod take traffic right now? | the Pod is removed from Service endpoints; nothing is restarted | dependency reachability, cache warmed, migrations done, draining |
| liveness | is this process wedged beyond recovery? | the container is restarted (after failureThreshold failures) | a check of the process itself: event loop alive, no deadlock |
| startup | has the process finished starting? | the container is restarted; until it succeeds, the other two are not run | the same check as liveness, with a long window |
Readiness talks to the Service; liveness talks to the kubelet’s restart logic; startup holds the other two back. The bottom row is the same database check in each of the first two: one removes Pods from traffic while the failover lasts, the other restarts the fleet.
The failure that turns a blip into an outage
A liveness handler that checks the database looks thorough and is a fleet-wide restart waiting to happen: the database pauses for a failover, every replica's liveness fails three times, the kubelet restarts all of them at once, and the service that would have recovered in ten seconds now spends two minutes rebuilding connection pools while the incident channel fills with a Kubernetes-shaped explanation of a database event. Readiness is the probe that may look outward, because its failure mode is stop sending me traffic, which is exactly right while a dependency is down. Liveness may only look inward.
readinessProbe:httpGet: { path: /ready, port: 8080 } # checks dependencies and warm-upperiodSeconds: 5failureThreshold: 3livenessProbe:httpGet: { path: /healthz, port: 8080 } # answers only: is this process responsiveperiodSeconds: 10timeoutSeconds: 2failureThreshold: 3terminationGracePeriodSeconds: 20 # a wedged process gets 20 s, not the Pod's 60 sstartupProbe:httpGet: { path: /healthz, port: 8080 }periodSeconds: 5failureThreshold: 36 # up to 180 s to come up before anything else runs
The startup probe is what makes a slow-booting JVM or .NET service survivable without loosening liveness for its whole life: during the failureThreshold × periodSeconds window only the startup probe runs, and once it succeeds the tight liveness settings take over. The run put two Pods with a 20-second boot next to each other under the same liveness probe (three failures, two seconds apart): the one without a startup probe was restarted after 9 s, before it had served anything, and was sitting in CrashLoopBackOff with two restarts a little later; the one with a startup probe was Ready after 12 s with zero restarts. The per-probe terminationGracePeriodSeconds on the liveness and startup probes shortens the shutdown for a process the kubelet is restarting because it is unresponsive; the Pod-level grace period still applies to ordinary terminations, where the process is expected to drain. That number is visible in the restart timing: with terminationGracePeriodSeconds: 5 on the probe, a liveness failure turned into a restarted container in 13 s (three failed probes, then the grace period); a busybox httpd that ignores SIGTERM would otherwise have waited out the Pod's 30 s.
How probes behave during a rollout and a shutdown
During a rolling update a new Pod failing readiness is the system working: it stays out of the endpoints until it is ready, and maxUnavailable keeps the old Pods serving; the run rolled out a template that never creates its readiness file, rollout status timed out with the new Pod at ready=false and both old Pods still ready=true in the slice, and kubectl rollout undo brought the Deployment back to two ready endpoints. A new Pod failing liveness during a rollout is a misconfiguration: the kubelet restarts it before it ever joins the Service, and the rollout stalls in CrashLoopBackOff with a healthy previous version still running, which is the moment to check whether a startup probe is missing. On the way out the order reverses: at termination the Pod is removed from endpoints and sent SIGTERM at about the same time, so a preStop hook that sleeps a few seconds, or an application that fails readiness on SIGTERM and keeps serving for the drain period, is what stops in-flight requests from hitting a Pod that is already gone from the load balancer's point of view.
kubectl get endpointslices -n shop -l kubernetes.io/service-name=api -o jsonpath="{range .items[*].endpoints[*]}{.targetRef.name} ready={.conditions.ready}{'\n'}{end}"api-b8dd6b8d-phhbs ready=trueapi-b8dd6b8d-sdvcr ready=truekubectl -n shop exec api-b8dd6b8d-phhbs -- rm /www/ready # the readiness endpoint now answers 404wait_for api-b8dd6b8d-phhbs "{.status.containerStatuses[0].ready}" false 30 # fixture helper: polls the field once a second{.status.containerStatuses[0].ready} = false after 4skubectl get endpointslices -n shop -l kubernetes.io/service-name=api -o jsonpath="…"api-b8dd6b8d-phhbs ready=falseapi-b8dd6b8d-sdvcr ready=truekubectl -n shop get pod api-b8dd6b8d-phhbs -o jsonpath="ready={.status.containerStatuses[0].ready} restarts={.status.containerStatuses[0].restartCount}"ready=false restarts=0Unhealthy Readiness probe failed: HTTP probe failed with statuscode: 404 # from kubectl get eventsone Pod out of rotation after failureThreshold x periodSeconds (2 x 2 s), zero restarts: readiness doing its job while a dependency recovers. touch /www/ready and it was back in the slice within a secondkubectl -n shop exec api-b8dd6b8d-phhbs -- rm /www/healthz # now the liveness endpoint answers 404wait_restart api-b8dd6b8d-phhbs 1 60 # fixture helper: polls restartCountrestartCount=1 after 13sUnhealthy Liveness probe failed: HTTP probe failed with statuscode: 404Killing Container api failed liveness probe, will be restartedready=true restarts=1 # the restarted container recreated its filesa RESTARTS column climbing on every replica at once is a liveness probe looking at something sharedkubectl -n shop patch deploy api --type=json -p='[{"op":"replace","path":"/spec/template/spec/containers/0/command","value":["sh","-c","mkdir -p /www && cd /www && touch healthz && exec httpd -f -p 8080 -h /www"]}]' # no /ready, everkubectl -n shop rollout status deploy/api --timeout=30s; echo exit=$?error: timed out waiting for the conditionexit=1kubectl get endpointslices -n shop -l kubernetes.io/service-name=api -o jsonpath="…"api-5b94988cb9-bzx7d ready=falseapi-b8dd6b8d-phhbs ready=trueapi-b8dd6b8d-sdvcr ready=truethe new Pod never joins the Service and is never restarted (its liveness passes); the old two keep serving. This is the rollout doing what readiness is forkubectl -n shop rollout undo deploy/api && kubectl -n shop rollout status deploy/api --timeout=120sdeployment "api" successfully rolled outapi-b8dd6b8d-phhbs ready=trueapi-b8dd6b8d-sdvcr ready=trueundo restores the previous template; once the NotReady Pod is gone the slice lists the two ready endpoints againWhen a probe change takes the service down: recovery in order of preference
| Symptom | Diagnosis | Reversal and verification |
|---|---|---|
a rollout stalls, rollout status times out, the new Pods are 0/1 Ready with no restarts | readiness never passes for the new template; kubectl describe pod shows Readiness probe failed | kubectl rollout undo deployment/<name>; verify with rollout status and the EndpointSlice showing only ready endpoints (executed) |
| every replica restarts within the same minute | liveness checks a shared dependency; kubectl get events shows Liveness probe failed on all of them at once | move the dependency check to readiness and redeploy; until then, raising failureThreshold on the liveness probe buys time without changing what is checked (documentation-backed) |
new Pods land in CrashLoopBackOff before ever serving | liveness fires during a slow boot; describe pod shows the restart before the first successful probe | add a startupProbe with a window longer than the boot (failureThreshold × periodSeconds) and redeploy; verify restartCount stays 0 while the Pod becomes Ready (executed on the fixture’s 20 s boot) |
| a Pod is Ready while returning errors | readiness is a static 200 and checks nothing the request path needs | make the readiness handler check what the request needs and redeploy; there is nothing to roll back, the probe was never doing its job (documentation-backed) |
Probe types, and the one to avoid
httpGet is the default choice: any 2xx or 3xx is success, and the handler can be as cheap as returning a static 200 for liveness. tcpSocket only proves the port is open, which a deadlocked server with an accept queue still passes. grpc probes speak the gRPC health protocol to services that have no HTTP endpoint. exec runs a command in the container, forks a process every period, and needs a shell or binary that a distroless image does not have; it is the probe type to justify rather than default to. Whatever the type, the check must be cheap and must not itself allocate the resources it is checking for.
Probes decide whether a Pod gets traffic and whether it is restarted. They do not decide whether it can be scheduled or how much it may consume (requests and limits take over there), nor how many replicas a node drain leaves running, the job of a PodDisruptionBudget. An image with no shell changes the probe choice too: the exec type is unavailable there, a limitation distroless images turn into a feature.