Health checks and rollouts
Thursday, 11:20: booking-api deploys ten times a day, and every deploy fails about 2% of requests for 40 seconds. At 1,500 requests a second, that's over a thousand errors per deploy.
Kubernetes asks your app three questions to decide whether to wait, hide it, or restart it. Get them right and deploys become boring.
- Problem
The booking API deploys 10 times a day across 12 pods, and nodes are drained weekly for upgrades. Guests must never see errors during any of it.
- Why the simple system fails
Naive probes restart healthy pods during a database blip, pods killed mid-request drop bookings, an aggressive rollout removes half the capacity at once, and a node drain evicts every replica together.
- Engineering concept
Separate the questions (am I alive? can I serve? have I finished booting?), drain before dying, bound how much capacity a rollout or drain may remove, and keep the previous version one command away.
- Technology
livenessProbe, readinessProbe, startupProbe, preStop + SIGTERM handling, terminationGracePeriodSeconds, maxSurge/maxUnavailable, minReadySeconds, progressDeadlineSeconds, PodDisruptionBudget, kubectl rollout undo.
- Trade-offs
Safer rollouts are slower and need spare capacity; sensitive probes detect failure fast but cause flapping and restarts; strict PDBs protect availability but can block node maintenance.
The problem
2 min- Omar · On-call SREEvery deploy: ~2% errors for ~40 s. booking-api runs 12 pods at about 1,500 requests a second.
- Maya · Product managerSo each deploy costs more than a thousand failed requests. Some are checkouts.
- Lina · Senior engineer, your mentorYou lead this one. Guess first.
The big idea
4 minReady before traffic, drained before exit. Each probe answers one question, and triggers one action.
Deploying less often isn't the fix: ten small deploys a day is a feature. Fix the arrival and the departure instead, and deploys become boring.
How it works
5 min- 1Boot
A asks "have you finished booting?" It gives booking-api up to 150 s; nothing else is checked until it passes.
- 2Join
A asks "can you take traffic now?" If not, the pod is hidden from the Service, not restarted.
/readyzsays yes only once the cache is warm (~40 s). - 3Serve
A asks "are you stuck for good?" If not answering, the container is restarted.
/livezchecks only that the event loop is alive. - 4Leave
On SIGTERM, a 5 s
preStoppause. Endpoint removal runs in parallel with SIGTERM, so traffic still arrives for a few seconds. - 5Drain and exit
Graceful shutdown: stop accepting, finish in-flight requests, close database connections, exit well within the 45 s grace period.
- 6Pace the rollout
maxSurge: 25%adds up to 3 extra pods (15 total);maxUnavailable: 0keeps at least 12 ready. Capacity never dips.
Watch a decision being made
4 minYou make booking-api deploys invisible
Every deploy fails ~2% of requests for ~40 s. Make deploys error-free without slowing them more than necessary. Lina pushes back in the "why" lines.
- 1Measure the warm-up
A fresh pod: process up at 3 s, cache warm at about 40 s. Worst case seen: 90 s on a busy node.
Why: Probe timings are maths on these numbers. Guessing them is how crash loops start.
- 2Write the endpoints and probes
/livez: 200 if the event loop answers./readyz: 200 only if the cache is warm and the pod isn't shutting down or overloaded. Startup probe every 5 s, up to 30 failures (150 s).tsif (req.url === "/readyz") { const ok = !shuttingDown && cacheWarm() && inFlight < 200; res.statusCode = ok ? 200 : 503; return void res.end(); }Why: "Readiness on local signals only. If it checked the shared database, a blip would empty the Service of every pod at once."
- 3Leave politely, and pace the rollout
SIGTERM → readiness fails,
server.close(), close the pool, exit.preStop: sleep 5, grace 45 s.maxSurge: 25%,maxUnavailable: 0,minReadySeconds: 10.Why: The pause covers the parallel endpoint removal; a pod crashing 5 s after ready doesn't count as done. Needs room for 3 extra pods.
- 4Prove it under load
Deploy during a 1,500 requests/s load test, then kill the database in staging and check that no pod restarts.
Why: Each test targets one failure you designed for. Result: about 5 failed requests per deploy, down from about 1,200.
In real life
3 minNetflix: automated canary analysis
- Has publicly described Spinnaker and Kayenta: a new version gets a small slice of traffic
- Its metrics are compared with the old version automatically
- Needs traffic splitting and solid metrics
Worth it for huge traffic where a bad minute is very expensive.
Etsy: deploy often, roll back fast
- Has publicly described deploying many times a day with a simple one-button tool
- Small changes, heavy monitoring, quick rollback
- A bad change reaches everyone for a few minutes
Worth it when changes are small and monitoring catches problems fast.
What goes wrong
3 minOne blip fails every pod at once, and they all restart cold.
Liveness checks only the process. Ask: would a restart fix this?
Traffic still arrives for a few seconds; in-flight requests are cut.
preStop pause, then stop accepting, finish in-flight work, exit before the grace period.
Rollback restores old code, not the old schema or data the new version wrote.
Backward-compatible migrations (expand, then contract).
Trade-offs
3 min| Settings (12 replicas) | Pods during rollout | Effect |
|---|---|---|
| Surge 25%, unavailable 25% | Up to 15; at least 9 ready | Fast; loses a quarter of capacity |
| Surge 25%, unavailable 0 | Up to 15; at least 12 ready | No capacity loss; room for 3 extra pods |
| Surge 50%, unavailable 0 | Up to 18; at least 12 ready | Faster, still safe; room for 6 extra pods |
- Karim · CTODeploys went from 90 seconds to 5 minutes. Ten a day. Use 50% surge and 25% unavailable.
- Omar · On-call SRE25% unavailable is 3 of 12 pods gone at peak. The other 9 run hot, fail readiness, and we're back to errors.
- Karim · CTOThen deploy outside peak hours.
Your job as tech lead
4 minClaude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.
- What each probe may check: liveness = "is this process stuck?" only; readiness = this pod's own ability to serve; never the shared database.
- Rollout pacing for 12 replicas at ~1,500 req/s:
maxSurge: 25%,maxUnavailable: 0, so capacity never drops below 12. - The trade you accept: ~5-minute deploys instead of ~1.5, for ~0 errors per deploy.
Fix booking-api's health checks: every deploy fails ~2% of requests for 40 s. I want a startup probe allowing up to 150 s; /livez checking only the process, no database; /readyz on local signals like cache warmed; preStop sleep 5 s; SIGTERM handling that finishes in-flight requests within the 45 s grace period; maxSurge 25%, maxUnavailable 0, minReadySeconds 10. Plan first. Then in staging, deploy under load and run a database failover, and show me the error rate during each.
- Read every probe handler: none touches PostgreSQL, Redis or another service.
- The deploy-under-load result: errors about zero, not 2% for 40 s.
- The failover drill: pods keep running (maybe returning errors), not all restarting at once.
- It adds the database to the liveness probe "to be thorough".
- It "fixes" crash loops by turning liveness off, or a huge
initialDelaySecondsinstead of a startup probe. - It claims zero-downtime deploys without having deployed under load.
Check yourself
2 minmaxSurge: 25% and maxUnavailable: 25%. During the rollout, what is the minimum number of available pods Kubernetes guarantees?Next episode: a three-person team wants its own Kubernetes cluster for a brand-new product. You lead the decision.
Mark it complete to update your skill map.
Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
Technical deep dive
You lead the fix. Lina's comments are in the "why" lines where she pushed back.
You make booking-api deploys invisible
Every booking-api deploy fails ~2% of requests for ~40 s. Make deploys and weekly node upgrades error-free without slowing them down more than necessary.
- 1Measure the warm-up
Time a fresh pod: process up at 3 s, pricing rules loaded and cache warm at about 40 s. Worst case seen: 90 s on a busy node.
Why: Probe timings are maths on these numbers. Guessing them is how crash loops start.
- 2Write three endpoints
/livez: returns 200 if the event loop answers./readyz: 200 only if rules are loaded, the cache is warm, and the pod isn't shutting down or overloaded.tsif (req.url === "/livez") return void res.end("ok"); if (req.url === "/readyz") { const ok = !shuttingDown && cacheWarm() && inFlight < 200; res.statusCode = ok ? 200 : 503; return void res.end(); }Why: Lina: "readiness on local signals only. If it checked the shared database, a database blip would empty the Service of every pod at once."
- 3Set the probes
Startup probe on
/livez: every 5 s, up to 30 failures (150 s). Liveness on/livez: every 10 s, 3 failures. Readiness on/readyz: every 5 s, 2 failures.Why: The startup budget (150 s) covers the 90 s worst case with room. Detection time is period × threshold: about 30 s to restart a truly stuck pod.
- 4Leave politely
On SIGTERM: mark shutting down (readiness fails),
server.close()to finish in-flight requests, close the pool, exit. AddpreStop: sleep 5andterminationGracePeriodSeconds: 45.Why: Endpoint removal runs in parallel with SIGTERM. The 5 s pause covers it; the drain must end before the 45 s grace period or the pod is killed anyway.
- 5Pace the rollout
maxSurge: 25%,maxUnavailable: 0,minReadySeconds: 10,progressDeadlineSeconds: 300. Add a PodDisruptionBudget withmaxUnavailable: 1for node upgrades.Why: Capacity never dips below 12 ready pods; a pod that crashes 5 s after becoming ready doesn't count; a stuck rollout is marked failed instead of hanging. Needs room for 3 extra pods.
- 6Prove it
Deploy during a load test at 1,500 requests/s and watch the error rate. Then drain a node. Then kill the database in staging and check that no pod restarts.
Why: Each test targets one failure mode you just designed for. Result: about 5 failed requests per deploy, down from about 1,200.
| Setting | Rollout with 12 replicas | Effect |
|---|---|---|
| maxSurge 25%, maxUnavailable 25% | Up to 15 pods; at least 9 available | Fast; temporarily loses 25% of capacity |
| maxSurge 25%, maxUnavailable 0 | Up to 15 pods; at least 12 available | No capacity loss; needs room for 3 extra pods |
| maxSurge 0, maxUnavailable 1 | At most 12 pods; at least 11 available | No extra nodes needed; slow, one pod at a time |
| maxSurge 100%, maxUnavailable 0 | Up to 24 pods | Near blue-green: whole new set first; doubles capacity briefly |
Interactive simulation
Visual diagram
One rolling-update step with maxSurge 1 and maxUnavailable 0, including graceful shutdown of the old pod. A small 4-pod example; booking-api runs 12 in summer.
Analogy
- A hotel front deskNew receptionist still logging inKubernetesStartup probe: don't judge yet
- A hotel front deskFlipping the desk sign to "closed"KubernetesReadiness: no traffic for now
- A hotel front deskReplacing someone who collapsedKubernetesLiveness: restart the container
- A hotel front deskFinish the guest in front, then leaveKubernetesGraceful shutdown
- A hotel front deskNever fewer than 10 desks openKubernetesA PodDisruptionBudget
Where the analogy breaks: you'd never fire a receptionist because the booking system is down. A badly written liveness check does exactly that, to every pod.
Check your understanding
livenessProbe:
httpGet: { path: /livez, port: 3000 }
periodSeconds: 10
failureThreshold: 3
# no startupProbe, no initialDelaySecondsPractice ladder
Trace one rollout cycle
Scenario Warm-upsearch-api has 8 replicas, maxSurge: 25%, maxUnavailable: 0, minReadySeconds: 10. New pods take 20 s to become ready. A pod of the new version crashes 5 s after becoming ready (a bug that only shows under traffic).
Trace the rollout: how many pods are created first, when old pods are removed, and what happens when the new pods crash.
Put the graceful shutdown in order
Scenario PracticeDuring a rollout, Kubernetes terminates an old booking-api pod. Put these events in order and fill in the missing step X:
- A. The process exits with code 0
- B. Kubernetes sends SIGTERM to the container (after preStop)
- C. The pod is marked Terminating; endpoint removal starts and the
preStophook (sleep 5) runs - D. The app sets
shuttingDown = trueand/readyzreturns 503 - E. The app closes its database pool
- X. ___
Also: what happens if the app hasn't exited 45 s after termination started?
The new version crash-loops
Debugging Practicepricing-api 4.0 loads a 2 GB pricing model at start: about 60 s on a quiet node, up to 100 s on a busy one. Its probes:
livenessProbe:
httpGet: { path: /livez, port: 8080 }
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet: { path: /readyz, port: 8080 }
periodSeconds: 5/livez only answers once the model is loaded. Pods are now in CrashLoopBackOff. Explain why and fix it.
Should readiness check the database?
Decision StretchThe hotel-page service (8 pods) shows hotel details. Hotel details are cached in each pod for 5 minutes; about 90% of requests hit the cache. A teammate proposes adding a database check to /readyz "so we don't serve when the database is down".
Facts: the database fails over about once a month for ~40 s. Without the check, during a failover, cached pages still load and the 10% of uncached pages fail with a friendly error.
Write a short decision: "We chose X over Y because…, accepting the risk of…".
Explain it yourself
Walk through what happens, step by step, when you deploy a new version of a 4-pod service with maxSurge 1 and maxUnavailable 0, and how you'd make sure no guest request fails.