Engineering Notes
Home/DevOps & Cloud/Observability/Health Checks
DevOps & Cloud · Observability

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.

☁️ DevOps & Cloud● Intermediate⏱ 4 min read▶ 1 interactive🛠️ 2 exercises🎤 5 interview Q&As

🎯 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:

ProbeQuestionIf it fails…Typical endpoint
livenessis the process stuck beyond recovery?the orchestrator restarts itGET /livez
readinessshould this instance receive traffic right now?removed from the load balancer / Service endpoints (not restarted)GET /readyz
startuphas slow initialization finished?liveness/readiness checks don't start until it passesGET /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

yaml
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: 40

4Graceful Shutdown Is Part of Health

text
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.

A request handler exposes livez that only checks the process, readyz that reflects warm-up, draining and critical resources, and a protected details endpoint; the kubelet restarts on liveness failure and removes the pod from endpoints on readiness failure, and SIGTERM flips readiness before draining
A request handler exposes livez that only checks the process, readyz that reflects warm-up, draining and critical resources, and a protected details endpoint; the kubelet restarts on liveness failure and removes the pod from endpoints on readiness failure, and SIGTERM flips readiness before draining

⚠️ Common Pitfalls

  1. Dependency checks in liveness — a DB blip restarts the whole fleet.
  2. Readiness that says "ready" before warm-up — the first requests time out.
  3. No startup probe for slow starters — liveness kills them before they ever start.
  4. Exiting immediately on SIGTERM — dropped requests every deploy.
  5. Expensive health endpoints — probes become a load source.
  6. Same endpoint for all three probes — different questions need different answers.

📋 Quick Reference

checksfailure action
livenessprocess alive, not deadlockedrestart
readinesswarmed up, not draining, can serveremove from LB
startupinit finisheddelays the other probes
detailsevery 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

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

1. A failing readiness probe causes…
Liveness failures restart.
2. Should liveness check the database?
Restarts don't fix dependencies.
3. A JVM app needs 90 s to boot and liveness kills it at 30 s. Fix?
Startup probes hold off liveness.
4. During shutdown, first…
Stop receiving new traffic first.