How do health checks and readiness/liveness probes actually work, and why are they different?
"Should this container be restarted" and "should traffic go here right now" are different questions with different remedies, and conflating them is how a database blip becomes a fleet-wide crash loop.
LEARN BY DOINGPut your intuition to work3 steps
How do health checks and readiness/liveness probes actually work, and why are they different?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Two different questions, two different probes
"Is this instance broken and should be restarted?" and "is this instance currently able to serve traffic?" sound similar but demand opposite actions, which is exactly why Kubernetes (and most orchestration platforms) expose them as two separate probes rather than one. A liveness probe answers the first question: if it fails repeatedly, the orchestrator kills and restarts the container, on the assumption that the process is stuck in a way only a restart fixes (a deadlock, a hung thread pool, a memory leak past the point of recovery). A readiness probe answers the second: if it fails, the orchestrator simply stops routing new traffic to that pod (removes it from the Service's endpoint list) without touching the container at all — the process keeps running, and traffic resumes automatically the moment the probe passes again. A third, less universally known one, the startup probe, exists purely to give slow-starting applications (a JVM warming up, a large cache to preload) a grace period before liveness checks even begin, so a legitimately-still-booting app isn't killed for failing a liveness check it was never going to pass yet.
#Why conflating them causes crash loops
The single most common mistake — and a great interview trap — is wiring the liveness probe to check downstream dependencies (a database connection, a downstream API). It seems reasonable ("if I can't reach the database, something's wrong!"), but it's precisely backwards: if the database has a brief blip, every pod in the deployment fails its liveness check simultaneously, the orchestrator restarts all of them at once, and now you've turned a transient database hiccup into a self-inflicted, fleet-wide outage — the pods come back up, immediately fail liveness again because the database still hasn't recovered, and you're in a crash-loop that makes the original problem worse and harder to diagnose. The database being briefly unreachable is a readiness concern (stop sending traffic here until it's back) not a liveness concern (this process itself is not broken, it just can't currently reach something external).
#Kubernetes configuration
apiVersion: apps/v1kind: Deploymentmetadata:name: checkout-servicespec:replicas: 4selector:matchLabels:app: checkout-servicetemplate:metadata:labels:app: checkout-servicespec:containers:- name: checkout-serviceimage: registry.example.com/checkout-service:1.14.2ports:- containerPort: 8080startupProbe:httpGet:path: /healthzport: 8080failureThreshold: 30 # allow up to 30 * 5s = 150s to bootperiodSeconds: 5livenessProbe:httpGet:path: /healthzport: 8080initialDelaySeconds: 0 # startupProbe already covered boot timeperiodSeconds: 10timeoutSeconds: 2failureThreshold: 3 # 3 consecutive misses -> restartreadinessProbe:httpGet:path: /readyzport: 8080initialDelaySeconds: 0periodSeconds: 5timeoutSeconds: 2failureThreshold: 2 # 2 consecutive misses -> drop from ServicesuccessThreshold: 1 # 1 success -> add back to Service
Notice the two probes point at different endpoints — /healthz and /readyz — on purpose; sharing one endpoint for both is how teams accidentally reintroduce the crash-loop problem above.
#The endpoint implementations
package mainimport ("encoding/json""net/http""sync/atomic")// draining is flipped to true during graceful shutdown so readiness// fails immediately and the load balancer stops sending new traffic,// while in-flight requests finish normally.var draining int32func healthzHandler(w http.ResponseWriter, r *http.Request) {// Liveness: only check that THIS process's core loop is responsive.// No downstream calls here, deliberately -- a slow dependency must// never be able to get this pod killed.w.WriteHeader(http.StatusOK)json.NewEncoder(w).Encode(map[string]string{"status": "alive"})}func readyzHandler(w http.ResponseWriter, r *http.Request) {if atomic.LoadInt32(&draining) == 1 {http.Error(w, "draining", http.StatusServiceUnavailable)return}if err := pingDatabase(); err != nil {http.Error(w, "database unreachable: "+err.Error(), http.StatusServiceUnavailable)return}if err := pingDownstreamPaymentsAPI(); err != nil {http.Error(w, "payments API unreachable: "+err.Error(), http.StatusServiceUnavailable)return}w.WriteHeader(http.StatusOK)json.NewEncoder(w).Encode(map[string]string{"status": "ready"})}func pingDatabase() error {// A cheap connectivity check against the connection pool,// not a full query -- e.g. db.PingContext(ctx) with a short timeout.return nil}func pingDownstreamPaymentsAPI() error {// A lightweight health check against a critical hard dependency.return nil}func main() {http.HandleFunc("/healthz", healthzHandler)http.HandleFunc("/readyz", readyzHandler)http.ListenAndServe(":8080", nil)}
The draining flag is worth calling out on its own: on SIGTERM, a well-behaved service flips it to true immediately, which makes /readyz start failing right away so the load balancer stops routing new requests, while the process keeps running long enough to finish requests already in flight before actually exiting — this is what makes rolling deploys avoid dropped requests, and it has nothing to do with the liveness probe at all.
🧠 Check your understanding
0/2 · 0/2 answered1. Why is it a mistake to make a liveness probe check the database connection?
2. What is the practical effect of a readiness probe failing, as opposed to a liveness probe failing?