Skip to main content

Health Checks, Draining & Graceful Rollout

Level 4: Level 4: Scaling Compute & Trafficmedium25 minhealth-checksdrainingdeploy

Separate liveness (restart) from readiness (pull from pool), drain in-flight work before terminating, slow-start cold nodes, and keep deep checks from failing the whole fleet.

Send traffic only to nodes that can serve it

Level 1's load-balancing lesson named health checks as the mechanism that keeps a pool healthy; this lesson is the deep treatment. A load balancer only helps if it sends traffic to nodes that can actually serve it and stops sending to nodes that cannot. That is the job of health checks, and the subtlety is doing it without evicting healthy nodes or dropping in-flight work during a deploy.

There are two ways to know a node is bad. Active checks have the LB probe each backend on an interval (an HTTP GET /healthz, a TCP connect) and mark it unhealthy after N consecutive failures: fast, proactive detection at the cost of probe traffic. Passive checks (outlier detection) observe real traffic: if a backend starts returning 5xx or timing out on actual requests, eject it from the pool for a cooldown. Passive checks catch failures a shallow probe misses. Production uses both.

Liveness vs readiness

  • Liveness asks "is this process alive at all?" A failed liveness check means the node is broken and should be restarted/replaced.
  • Readiness asks "is this node ready to receive traffic right now?" A node can be alive but not ready: still warming its cache, loading a model, filling connection pools, or temporarily shedding load. A not-ready node should be pulled from the LB pool but not killed.
Check yourself
A new node needs 90 seconds to warm its cache. The platform uses one '/health' endpoint for both liveness and readiness, and it returns failure until the cache is warm. What happens on startup?

Conflating them is a classic bug. If you treat "not warmed up yet" as a liveness failure, the orchestrator keeps killing and restarting perfectly good nodes in a crash loop. Gate a newly started node behind readiness until it is warm, then admit it.

Deploys: draining and slow-start

  • Connection draining (graceful shutdown): when a node is going away, first mark it not ready so the LB stops sending it new requests, but let its in-flight requests (and long-lived streams) finish up to a drain timeout before the process exits. The sequence: stop advertising -> stop new traffic -> wait for in-flight to complete (or hit the deadline) -> terminate.
  • Slow-start / ramp: a freshly joined node starts with zero warm cache and cold connection pools. If the LB immediately gives it a full 1/N share, it can fall over or spike latency. Slow-start ramps its traffic share up over some seconds.
Check yourself
A node's '/healthz' returns 200 whenever the web process is running. Its database connection pool is dead. What does the active health check conclude?

Interview nuance: deep vs shallow health checks. A shallow check returns 200 as long as the web server is up, even if the database or a critical downstream is unreachable, so the node keeps receiving traffic and failing every real request. A deep check verifies the critical dependencies. But deep checks have their own trap: if every node's health check hits a shared dependency and that dependency blips, every node marks itself unhealthy at once and the whole fleet drops out, turning a minor blip into a total outage. The mature answer: deep enough to catch a truly broken node, with hysteresis, and not so coupled that a shared-dependency blip fails the entire fleet simultaneously.

  drain sequence on node removal / deploy:
  mark NOT-READY -> LB stops NEW traffic -> in-flight finishes (<= drain deadline) -> terminate
  join sequence:
  start -> READINESS gates traffic until warm -> slow-start ramps share up

Recap: use active probes plus passive outlier ejection; keep liveness (restart) separate from readiness (pull from pool, do not kill); drain connections and slow-start new nodes so a rolling deploy drops nothing; and make checks deep enough to catch a broken downstream without letting one shared-dependency blip fail the whole fleet at once.

Check yourself
After the shallow-check incident, every node's check is upgraded to deeply verify the shared database. The database then blips for ten seconds. What does the load balancer see?

Apply

Your turn

The task this lesson builds to.

Design health checking and drain behavior so a rolling deploy of 50 nodes never drops in-flight requests or a long-lived stream.

Think about

  1. What is the difference between liveness and readiness?
  2. How do connection draining and slow-start protect requests?
  3. Why can a shallow 200 mask a broken dependency?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design the health-check and rollout strategy for a Kubernetes fleet of 500 pods behind an Envoy mesh where a critical shared dependency (a central auth service) occasionally has a 10-second blip, and explain how you avoid a deep health check turning that blip into a fleet-wide outage.

Think about

  1. What happens if all 500 readiness probes hard-depend on the shared auth service?
  2. What do ejection caps and probe hysteresis each protect against?
  3. How can the sidecar make a 10-second auth blip invisible to requests?