Health checks
Liveness and readiness, gently.
An app can be running and still be useless. The process is alive, the container has not crashed, and yet the thing is frozen: stuck waiting on a database that never answers, or so swamped it cannot take another request. Kubernetes cannot see any of that by itself. Left alone, the only question it can answer is whether your program is still a process. Health checks are how you teach it to ask better questions.
A nurse doing rounds on a hospital ward stops at every bed and asks two things: are you alive, and are you well enough to see visitors? That is the shape of a health check in Kubernetes. The technical name is a probe, a small check the cluster repeats on a timer, and it asks your app those same two questions over and over for as long as it runs.
One piece of the cluster does the walking. On every node (a machine where Pods run) there is a kubelet, a small agent that starts your containers and keeps an eye on them. The kubelet is the nurse on rounds. It runs the probes, reads the answers, and acts on them.
Liveness and readiness
"Are you alive?" is the liveness probe. When it fails, Kubernetes concludes the app has hung and restarts the container to give it a clean start. That is how a frozen app gets rescued even though its process never actually crashed.
"Are you well enough for visitors?" is the readiness probe. When it fails, Kubernetes stops sending that Pod traffic and leaves it running. No restart, no drama. It waits for the Pod to report ready again. A Service (Kubernetes' built-in load balancer across your Pods) quietly routes around the unready one while that happens.
The two pull in opposite directions. Liveness unsticks a hung app by restarting it. Readiness keeps callers away from a Pod that is in no state to answer, whether it is still booting or briefly buried in work. Most of the time you want both.
A probe can ask its question in three ways. It can send an HTTP request (the same kind of web request your browser sends) to a health URL and pass when a good response comes back. It can knock on a port with a plain TCP connection: TCP, short for Transmission Control Protocol, is the ordinary way two programs open a network conversation, and a port is the numbered slot a program listens on. Or it can run a command inside the container and pass when that command exits cleanly. Real apps usually serve small dedicated URLs for this, /healthz for liveness and /ready for readiness, so each probe checks exactly what it is meant to.
Try it
Here is a Deployment you can apply as-is. A Deployment is the object that keeps a set of identical Pods running for you; this one runs two copies of nginx (a small, dependable web server) with both probes wired up. Save it as probes-demo.yaml. YAML is a plain-text format where indentation carries meaning, so copy the spacing exactly as shown.
apiVersion: apps/v1kind: Deploymentmetadata:name: weblabels:app: webspec:replicas: 2selector:matchLabels:app: webtemplate:metadata:labels:app: webspec:containers:- name: webimage: nginx:1.27ports:- containerPort: 80readinessProbe:httpGet:path: /port: 80initialDelaySeconds: 5periodSeconds: 10livenessProbe:httpGet:path: /port: 80initialDelaySeconds: 10periodSeconds: 10
Two settings do the work here. initialDelaySeconds is how long Kubernetes waits after the container starts before it probes at all, which gives a slow app room to boot. periodSeconds is how often it checks after that. So readiness starts at 5 seconds, liveness at 10, and both repeat every 10. Both point at /, a page nginx already serves, so the demo needs nothing else set up. A real app would point them at dedicated URLs like /healthz and /ready.
Send it to the cluster with kubectl, the command-line tool you use to talk to Kubernetes.
kubectl apply -f probes-demo.yaml
deployment.apps/web created
Now see what came up.
kubectl get pods -l app=web
NAME READY STATUS RESTARTS AGEweb-6d4c8f5b7c-2xk9p 1/1 Running 0 22sweb-6d4c8f5b7c-8ftlm 1/1 Running 0 22s
READY shows 1/1, meaning the one container in each Pod is passing its readiness probe. For the first few seconds it would have read 0/1, while the probe had not passed yet. That is Kubernetes holding traffic back on purpose until the app says it can cope.
To see how the probes are set up, and what the kubelet has been doing about them, describe one of those Pods by name.
kubectl describe pod web-6d4c8f5b7c-2xk9p
Name: web-6d4c8f5b7c-2xk9pNamespace: defaultStatus: RunningContainers:web:Image: nginx:1.27Port: 80/TCPState: RunningReady: TrueRestart Count: 0Liveness: http-get http://:80/ delay=10s timeout=1s period=10s #success=1 #failure=3Readiness: http-get http://:80/ delay=5s timeout=1s period=10s #success=1 #failure=3Events:Type Reason Age From Message---- ------ ---- ---- -------Normal Scheduled 41s default-scheduler Successfully assigned default/web-6d4c8f5b7c-2xk9p to node-1Normal Pulled 40s kubelet Container image "nginx:1.27" already present on machineNormal Created 40s kubelet Created container webNormal Started 40s kubelet Started container web
The Liveness and Readiness lines spell out each probe's settings. That #failure=3 means Kubernetes puts up with three checks failing back to back before it does anything, so a single blip costs you nothing. Below them, the Events are the kubelet's own account of how this Pod got running.
When liveness turns on a healthy app
That tolerance of three matters, because a wrong liveness probe is the one that bites. Every failure it counts moves the container closer to a restart. The classic beginner slip is copying the tidy /healthz pattern onto an app that does not serve it. Plain nginx has no /healthz page, so it answers every check with a 404 (the HTTP code for "no such page"), and a probe reads 404 as a failure. Aim liveness there and Kubernetes will restart a perfectly healthy app, again and again. Here is that mistake in one manifest.
apiVersion: apps/v1kind: Deploymentmetadata:name: web-looplabels:app: web-loopspec:replicas: 1selector:matchLabels:app: web-looptemplate:metadata:labels:app: web-loopspec:containers:- name: webimage: nginx:1.27ports:- containerPort: 80readinessProbe:httpGet:path: /port: 80livenessProbe:httpGet:path: /healthzport: 80periodSeconds: 5failureThreshold: 2timeoutSeconds: 1
Readiness still points at /, which nginx serves, so it keeps passing. Liveness points at /healthz, which nginx has never heard of. And periodSeconds: 5 with failureThreshold: 2 means two failed checks are all it takes to trigger a restart. Apply it, give it a minute or two, then list the Pod.
kubectl apply -f liveness-too-strict.yaml
deployment.apps/web-loop created
kubectl get pods -l app=web-loop
NAME READY STATUS RESTARTS AGEweb-loop-5f8c6d9b74-7t2qn 1/1 Running 4 (12s ago) 3m28s
Read that row carefully. READY is 1/1 and STATUS is Running, so the app is up and passing readiness. Yet RESTARTS reads 4 and keeps climbing. A healthy Pod with a restart count that will not stop rising is the fingerprint of a liveness probe firing at an app that is fine. Describe the Pod and the reason is right there.
kubectl describe pod web-loop-5f8c6d9b74-7t2qn
Events:Type Reason Age From Message---- ------ ---- ---- -------Warning Unhealthy 9s (x11 over 3m) kubelet Liveness probe failed: HTTP probe failed with statuscode: 404Normal Killing 9s (x4 over 3m) kubelet Container web failed liveness probe, will be restarted
Two lines tell the whole story. Liveness probe failed ... statuscode: 404 says the check is wrong, not the app: nginx returned 404 because there is no /healthz for it to serve. Killing ... will be restarted is the kubelet doing exactly what you asked of it, the same agent that keeps your app alive now knocking it down. The fix is to point liveness at something the app genuinely serves. If the app is slow to boot, add a startupProbe as well: Kubernetes runs that check alone at first and holds liveness back until it passes, so a long start is never mistaken for a hang.
apiVersion: apps/v1kind: Deploymentmetadata:name: web-looplabels:app: web-loopspec:replicas: 1selector:matchLabels:app: web-looptemplate:metadata:labels:app: web-loopspec:containers:- name: webimage: nginx:1.27ports:- containerPort: 80readinessProbe:httpGet:path: /port: 80startupProbe:httpGet:path: /port: 80failureThreshold: 30periodSeconds: 2livenessProbe:httpGet:path: /port: 80periodSeconds: 10failureThreshold: 3
kubectl apply -f liveness-fixed.yaml
deployment.apps/web-loop configured
kubectl get pods -l app=web-loop
NAME READY STATUS RESTARTS AGEweb-loop-6c7b5d4f98-nq8pd 1/1 Running 0 38s
Same app, same image, one probe changed, and the restarts stopped. Liveness is a powerful switch. Point it at something the app never serves and you have built yourself a machine for killing healthy Pods.
Where this pays off
You have already met rolling updates, where Kubernetes swaps old Pods for new ones a few at a time. Readiness is what makes that safe. A new Pod counts as ready only once its probe passes, so a broken version stalls the rollout instead of quietly taking the place of every healthy Pod you had. Day to day, readiness keeps traffic off Pods that are still starting or briefly overloaded, so nobody on the other end sees an error. Liveness works away in the background, restarting apps that have silently hung, catching the failures that plain crash-detection sails straight past.
Keep the two questions apart in your head and most probe decisions answer themselves. Liveness: should this container be restarted? Readiness: should this container get traffic? A startupProbe buys a slow app time before liveness is allowed to swing. And make readiness mean something. A probe that lightly touches a real dependency tells you the truth, while one hard-coded to answer 200 OK will keep insisting everything is fine while your app drowns.
There is a nastier version of the same trap. If your liveness probe has to take a lock that gets contended under load, heavy traffic makes the probe time out, and Kubernetes starts restarting Pods that were only busy. That is an outage you inflicted on yourself. Set periodSeconds and failureThreshold against what production traffic actually looks like, not against a quiet laptop cluster.
One habit for when you are on call. If a Service has no endpoints while its Pods all say Running, do not reach for the scale command. Check the Ready condition and the readiness probe failures in the describe events first. More Pods that also fail readiness gets you more of nothing.
Try this
Your turn. Add a readiness probe to a running Deployment, watch the Pod flip to Ready only once the probe passes, then check that its address shows up in the endpoint list. For a second round, point the path at something nginx does not serve and watch those endpoints empty out.
$ kubectl create deployment probed --image=nginx:1.27 --replicas=1deployment.apps/probed created$ kubectl patch deployment probed --type='json' -p='[{"op":"add","path":"/spec/template/spec/containers/0/readinessProbe","value":{"httpGet":{"path":"/","port":80},"initialDelaySeconds":2,"periodSeconds":5}}]'deployment.apps/probed patched$ kubectl get pods -l app=probed -wNAME READY STATUS RESTARTS AGEprobed-… 0/1 Running 0 3sprobed-… 1/1 Running 0 8s# Ctrl+C$ kubectl get endpoints probedNAME ENDPOINTS AGEprobed 10.244.1.30:80 20s$ kubectl delete deployment probeddeployment.apps "probed" deleted
Takeaway
The skill worth carrying out of this lesson is reading the symptoms. A Pod that is Ready with a climbing RESTARTS count means liveness is pointed somewhere wrong. Running Pods behind a Service with no endpoints mean readiness never passed. And a probe wired to answer yes no matter what will hide both of those from you until the pager goes off.