Health Checks
Liveness, readiness and startup probes answer three different questions, and mixing them up causes outages. See what happens when the database blips with a well-designed and a badly designed liveness probe, and make graceful shutdown part of health.
🎯 What you'll learn
- Liveness vs readiness vs startup: three questions
- What each probe should (and must not) check
- Kubernetes and load balancer probe configuration
- Graceful shutdown as part of health
📚 Before you start
1Three Different Questions
"Is the service healthy?" is actually three questions with different consumers and consequences:
| Probe | Question | If it fails… | Typical endpoint |
|---|---|---|---|
| liveness | is the process stuck beyond recovery? | the orchestrator restarts it | GET /livez |
| readiness | should this instance receive traffic right now? | removed from the load balancer / Service endpoints (not restarted) | GET /readyz |
| startup | has slow initialization finished? | liveness/readiness checks don't start until it passes | GET /startupz (or reuse livez) |
Load balancers' target health checks (ALB, NLB) are readiness checks by another name.
2What Each Should Check
Liveness — keep it dumb. Return 200 if the process can serve a trivial request (event loop not blocked, no deadlock). Never check dependencies in liveness: if the database is down, restarting every app instance doesn't fix the database — it turns a partial outage into a total one (crash loops, cold caches, thundering herds on recovery).
Readiness — "can I do useful work?":
- startup work finished (config loaded, caches warmed, migrations checked)
- not draining for shutdown
- critical local resources OK (connection pool can hand out a connection)
- dependencies: only hard dependencies without which every request would fail, and with care — if all instances fail readiness because a shared dependency is down, the LB has zero targets (some LBs then "fail open" and send traffic anyway). Often it's better to stay ready and return fast, meaningful errors (circuit breaker) than to disappear.
Deep health / diagnostics (/health/details, auth-protected): every dependency with latency and status — for humans and dashboards, not for automated restarts.
3Kubernetes Configuration
startupProbe:
httpGet: { path: /livez, port: 8080 }
periodSeconds: 5
failureThreshold: 30 # up to 150 s to start before liveness takes over
livenessProbe:
httpGet: { path: /livez, port: 8080 }
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3 # restarted after ~30 s of failures
readinessProbe:
httpGet: { path: /readyz, port: 8080 }
periodSeconds: 5
failureThreshold: 2
lifecycle:
preStop: { exec: { command: ["sleep", "10"] } } # let endpoints update before shutdown starts
terminationGracePeriodSeconds: 404Graceful Shutdown Is Part of Health
SIGTERM received
→ readiness starts returning 503 (stop getting NEW traffic)
→ wait until the LB/endpoints have removed the instance (preStop sleep / a few seconds)
→ stop accepting new connections, finish in-flight requests (bounded time)
→ close DB pools, flush logs/telemetry
→ exit 0 (before terminationGracePeriodSeconds, or SIGKILL arrives)Without this, every deploy produces a burst of 502s (system-design/system-design/08-service_discovery exercise 1).
5Implementation Tips
- Health endpoints must be cheap and fast (no heavy queries), have timeouts for anything they check, and ideally serve cached results computed in the background, so a burst of probes can't overload a dependency.
- Exclude them from auth, rate limiting, request logs (noise) and tracing sampling.
- Return machine-readable JSON (
{"status":"fail","checks":{...}}) with correct HTTP codes (200 / 503). - Expose them on the internal port only; don't leak dependency details publicly.
Under the HoodHow the Kubelet and an ALB Act on Probes▾
The kubelet on each node runs probes for its pods on the configured period. Consecutive failures reaching failureThreshold change the container's status: a liveness failure makes the kubelet kill and restart the container (with exponential back-off between restarts — the CrashLoopBackOff you see); a readiness failure flips the pod's Ready condition, the EndpointSlice controller removes the pod IP from the Service's endpoints, and kube-proxy/ingress controllers stop routing new connections to it (existing connections aren't cut). An ALB runs its own health checks from every load balancer node; a target becomes unhealthy after UnhealthyThresholdCount consecutive failures, and if all targets in a group are unhealthy, the ALB fails open and routes to all of them — a deliberate choice to avoid a total outage caused by an over-eager health check.
⚠️ Common Pitfalls
- Dependency checks in liveness — a DB blip restarts the whole fleet.
- Readiness that says "ready" before warm-up — the first requests time out.
- No startup probe for slow starters — liveness kills them before they ever start.
- Exiting immediately on SIGTERM — dropped requests every deploy.
- Expensive health endpoints — probes become a load source.
- Same endpoint for all three probes — different questions need different answers.
📋 Quick Reference
| checks | failure action | |
|---|---|---|
| liveness | process alive, not deadlocked | restart |
| readiness | warmed up, not draining, can serve | remove from LB |
| startup | init finished | delays the other probes |
| details | every dependency (protected) | humans/dashboards only |
🧠 Key takeaways
- Liveness: is the process stuck? (restart). Readiness: can it serve now? (remove from traffic). Startup: still booting.
- Liveness must never check dependencies; readiness may check critical ones.
- Tune thresholds and timeouts to avoid flapping; load balancer health checks act like readiness.
- On SIGTERM: fail readiness, drain in-flight work, then exit (preStop sleep for endpoint propagation).
🎤 Interview Q&A
Q1Liveness vs readiness checks?core▾
Liveness: is the process alive and not deadlocked? Failure → restart. Readiness: can it serve traffic now (warmed up, dependencies reachable)? Failure → stop sending traffic, no restart.
- Should liveness check the database? No; a DB outage would cause restart loops.
Q2What should a readiness check verify?core▾
That the instance can serve requests: initialization done, critical config loaded, essential dependencies reachable (maybe), not shutting down. Keep it fast and cheap.
- Readiness during shutdown? Fail it immediately on SIGTERM so the LB drains the instance.
Q3What are the risks of deep health checks?senior▾
A shared dependency failure makes every instance unhealthy at once, so the load balancer removes them all, turning partial degradation into a full outage. Load from health checks can also strain dependencies.
- Mitigation? Shallow checks for LB routing; deep checks for monitoring/alerting.
Q4How do health checks differ from synthetic monitoring?core▾
Health checks are internal signals for orchestration; synthetic monitoring runs real user-like transactions from outside (log in, search, checkout) to verify the system works end to end.
- Example tools? CloudWatch Synthetics, Checkly, Datadog Synthetics.
Q5How should a health endpoint be exposed?core▾
On a separate path (/healthz, /readyz) or port, not requiring authentication from the orchestrator, but not exposing sensitive details publicly.
- ASP.NET Core support?
AddHealthChecks()with tags for liveness/readiness.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.