{"id":"l-dk-5-1","courseId":"docker-kubernetes","lang":"en","kind":"lesson","summary":"Add startup, readiness and liveness probes to notes-api that tell Kubernetes the truth, and shut Pods down without dropping requests using a preStop pause and a SIGTERM handler.","html":"<p>Kubernetes can’t see inside your app. Left alone, it considers a container healthy if its process is running, even if that process is deadlocked, still warming up, or unable to reach the database. Probes are how the app tells the truth about itself. The truth has to be carefully worded, though, because Kubernetes acts on it automatically and at scale.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Three probes, three consequences</h2>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Probe</th>\n<th>Question it answers</th>\n<th>When it fails</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>startup</strong></td>\n<td>Has the app finished starting?</td>\n<td>Keep waiting, up to the limit, then restart</td>\n</tr>\n<tr>\n<td><strong>readiness</strong></td>\n<td>Should this Pod receive traffic right now?</td>\n<td>Remove the Pod from Service endpoints; no restart</td>\n</tr>\n<tr>\n<td><strong>liveness</strong></td>\n<td>Is the process stuck beyond recovery?</td>\n<td>Restart the container</td>\n</tr>\n</tbody>\n</table></div><p>While a startup probe is configured and hasn’t yet succeeded, the other two don’t run. Once it succeeds, it never runs again for that container.</p>\n<p>notes-api already has the two endpoints this needs. <code dir=\"ltr\">/healthz</code> returns <code dir=\"ltr\">ok</code> if the event loop can answer a request at all. <code dir=\"ltr\">/readyz</code> also checks the database pool and returns 503 if Postgres is unreachable.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">k8s/deployment.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">api</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ghcr.io/skylane/notes-api:1.4.0</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          ports</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">            - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">http</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              containerPort</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3000</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          startupProbe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            httpGet</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/healthz</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              port</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">http</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            periodSeconds</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            failureThreshold</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">30</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">      # up to 60 s to start</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          readinessProbe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            httpGet</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/readyz</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              port</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">http</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            periodSeconds</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            failureThreshold</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          livenessProbe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            httpGet</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/healthz</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              port</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">http</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            periodSeconds</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            timeoutSeconds</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            failureThreshold</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span></span></code></pre></div>\n<p>Any HTTP status from 200 to 399 is a pass. Besides <code dir=\"ltr\">httpGet</code> there are <code dir=\"ltr\">tcpSocket</code>, <code dir=\"ltr\">grpc</code> and <code dir=\"ltr\">exec</code> probes; prefer HTTP for web services, since it tests the path real traffic takes. The defaults, if you leave fields out, are <code dir=\"ltr\">periodSeconds: 10</code>, <code dir=\"ltr\">timeoutSeconds: 1</code> and <code dir=\"ltr\">failureThreshold: 3</code>. The one-second timeout catches slow apps by surprise, so set it deliberately.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Readiness is a traffic switch</h2>\n<p>Readiness is the probe that matters most day to day. A Pod that’s starting, overloaded or cut off from its database reports not ready and drops out of the Service’s endpoints until it recovers. It also <strong>gates rollouts</strong>: a new Pod only counts as available once it’s ready, and a rolling update removes old Pods only as fast as new ones become available. A release that can’t reach the database never takes traffic from a version that can. That’s why the Deployment without a readiness probe is the one I flag first in every review.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>A liveness probe that checks dependencies</span></p>\n<p>Pointing liveness at <code dir=\"ltr\">/readyz</code> feels thorough. Then Postgres restarts for 20 seconds, every notes-api container fails liveness at the same moment, and Kubernetes restarts all of them. When they come back, they all reconnect at once. A 20-second database blip has become a five-minute outage. Liveness asks one narrow question: is this process stuck? Anything about other systems belongs in readiness.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Startup without restart loops</h2>\n<p>Before startup probes existed, slow-starting apps were protected with <code dir=\"ltr\">initialDelaySeconds</code> on liveness. That delays the first check on <em>every</em> start by the worst-case startup time, and breaks again when startup gets slower. A startup probe gives the app <code dir=\"ltr\">periodSeconds × failureThreshold</code> (here 2 × 30 = 60 seconds) to come up, checking frequently so a fast start is detected quickly, and then hands over to liveness at its normal pace.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Shutting down without dropping requests</h2>\n<p>When a Pod is deleted, during a rollout, a scale-down or a node drain, two things start <strong>at the same time</strong>: the Pod is removed from the Service’s endpoints, and the kubelet begins terminating the container. Removing endpoints has to propagate to every node’s networking rules and to any load balancer, which takes a moment. If your app stops accepting connections the instant SIGTERM arrives, requests routed in that moment are refused.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 250\" role=\"img\" aria-labelledby=\"l-dk-5-1-t1\"><defs><marker id=\"arrow-l-dk-5-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-dk-5-1-t1\">At Pod deletion, endpoint removal propagates over a few seconds while the preStop sleep runs. Then SIGTERM is sent and the app drains in-flight requests and exits, all within the 30 second grace period, after which SIGKILL would be sent.</title>\n  <path class=\"d-line\" d=\"M40 40 L40 210\"/>\n  <text class=\"d-label-strong\" x=\"46\" y=\"30\">delete Pod</text>\n  <rect class=\"d-box-warn\" x=\"40\" y=\"60\" width=\"200\" height=\"40\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"140\" y=\"85\" text-anchor=\"middle\">endpoints removed</text>\n  <rect class=\"d-box-accent\" x=\"40\" y=\"120\" width=\"160\" height=\"40\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"120\" y=\"145\" text-anchor=\"middle\">preStop sleep 5s</text>\n  <path class=\"d-arrow\" d=\"M200 140 L236 140\" marker-end=\"url(#arrow-l-dk-5-1-1)\"/>\n  <rect class=\"d-box-success\" x=\"240\" y=\"120\" width=\"230\" height=\"40\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"355\" y=\"145\" text-anchor=\"middle\">SIGTERM: drain, then exit 0</text>\n  <path class=\"d-line d-dashed\" d=\"M40 190 L650 190\"/>\n  <path class=\"d-line\" d=\"M650 110 L650 200\"/>\n  <text class=\"d-label-muted\" x=\"345\" y=\"215\" text-anchor=\"middle\">terminationGracePeriodSeconds: 30</text>\n  <text class=\"d-label-strong\" x=\"650\" y=\"100\" text-anchor=\"middle\">SIGKILL</text>\n</svg><figcaption>Endpoint removal and termination start together; a preStop pause covers the gap</figcaption></figure>\n<p>The fix has two halves. In the Pod spec, pause before SIGTERM so routing catches up:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">k8s/deployment.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    spec</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      terminationGracePeriodSeconds</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">30</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      containers</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">api</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          lifecycle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            preStop</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              sleep</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">                seconds</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span></span></code></pre></div>\n<p>The <code dir=\"ltr\">sleep</code> action runs in the kubelet, so it works even in a distroless image with no <code dir=\"ltr\">sleep</code> binary. In the app, keep the SIGTERM handler from lesson 1.4: stop accepting new connections, finish in-flight requests, close the database pool, exit. The preStop pause and the drain must together fit inside <code dir=\"ltr\">terminationGracePeriodSeconds</code>, or the kubelet sends SIGKILL at the deadline.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Test it, don’t trust it</span></p>\n<p>Run a load generator against the Service and trigger <code dir=\"ltr\">kubectl rollout restart deployment/notes-api</code>. Count the errors. Zero is achievable, and once you’ve seen it you’ll notice immediately when a change breaks it.</p>\n</aside>\n<p>Kubernetes now knows when notes-api is healthy and how to stop it gracefully. Next, it needs to know how much CPU and memory notes-api uses, and how to add Pods when traffic grows.</p>\n","toc":[{"id":"sec-1","text":"Three probes, three consequences","level":2},{"id":"sec-2","text":"Readiness is a traffic switch","level":2},{"id":"sec-3","text":"Startup without restart loops","level":2},{"id":"sec-4","text":"Shutting down without dropping requests","level":2}],"takeaways":["A failing readiness probe removes the Pod from Service endpoints; a failing liveness probe restarts the container; a startup probe holds both off until the app has started.","Liveness checks only whether the process itself is stuck; never make it depend on a database or another service.","Readiness gates rolling updates, so a new version that can’t serve never replaces a working one.","On deletion, endpoint removal and SIGTERM happen at the same time; a short <code dir=\"ltr\">preStop</code> sleep lets traffic drain before the app starts shutting down.","The app must finish in-flight work within <code dir=\"ltr\">terminationGracePeriodSeconds</code> (30 by default) or it is killed."],"further":[{"title":"Liveness, Readiness, and Startup Probes","url":"https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/"},{"title":"Configure Liveness, Readiness and Startup Probes","url":"https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/"},{"title":"Pod Lifecycle (termination of Pods)","url":"https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination"}],"quiz":[{"q":"<p>notes-api’s readiness probe starts failing on one Pod because it can’t reach Postgres. What does Kubernetes do?</p>","options":[{"text":"Restarts the container until the probe passes.","why":"Restarts come from liveness failures. Readiness only affects traffic."},{"text":"Deletes the Pod and schedules a new one on another node.","why":"Probes never delete Pods. The Pod stays where it is."},{"text":"Nothing until the liveness probe also fails.","why":"Readiness acts on its own, independently of liveness."},{"text":"Removes the Pod from the Service’s endpoints, so it gets no new traffic until the probe passes again.","why":"Correct. The Pod keeps running and rejoins as soon as it’s ready, with no restart."}],"answer":3},{"q":"<p>Your liveness probe calls <code dir=\"ltr\">/readyz</code>, which checks the database. Postgres restarts for 20 seconds. What happens?</p>","options":[{"text":"Every notes-api container fails liveness and is restarted at once, turning a brief database blip into a full outage.","why":"Correct. Liveness should only ask “is this process stuck?” Dependency checks belong in readiness."},{"text":"The Pods are marked not ready and recover when Postgres returns.","why":"That would be true for a readiness probe. Liveness failures cause restarts."},{"text":"Kubernetes detects the database restart and pauses the probes.","why":"Kubernetes knows nothing about your app’s dependencies; it runs the probe as configured."},{"text":"Only one Pod restarts, because liveness failures are rate-limited.","why":"There’s no such cluster-wide rate limit. Each Pod’s probe fails independently, all at the same time."}],"answer":0},{"q":"<p>notes-api takes up to 60 seconds to start on a big database because it warms a cache. Which probe setup avoids a restart loop without slowing failure detection later?</p>","options":[{"text":"<code dir=\"ltr\">initialDelaySeconds: 60</code> on the liveness probe.","why":"It works, but every start now waits 60 seconds before liveness runs at all, and it breaks again when startup takes 65."},{"text":"Remove the liveness probe.","why":"Then a deadlocked process is never restarted."},{"text":"A startup probe with <code dir=\"ltr\">periodSeconds: 5</code> and <code dir=\"ltr\">failureThreshold: 15</code>, so the app gets up to 75 seconds to start before liveness takes over.","why":"Correct. Liveness and readiness are held off until the startup probe succeeds once; after that, liveness runs at its normal pace."},{"text":"<code dir=\"ltr\">timeoutSeconds: 60</code> on the liveness probe.","why":"That lets a single check hang for a minute, which delays detecting real hangs and doesn’t fix startup."}],"answer":2},{"q":"<p>During every rollout a few requests to notes-api fail with <code dir=\"ltr\">connection refused</code>, even though the app handles SIGTERM. What’s the usual fix?</p>","options":[{"text":"Raise <code dir=\"ltr\">terminationGracePeriodSeconds</code> to 300.","why":"The app already shuts down within the grace period. The errors happen at the start of termination, not the end."},{"text":"Add a <code dir=\"ltr\">preStop</code> sleep of a few seconds so endpoint removal propagates before the app stops accepting connections.","why":"Correct. SIGTERM and endpoint removal happen in parallel; a short pause lets proxies stop routing to the Pod first."},{"text":"Set <code dir=\"ltr\">replicas: 1</code> during rollouts.","why":"Fewer replicas makes any dropped connection hurt more, not less."},{"text":"Disable the readiness probe during rollouts.","why":"Readiness is what keeps unready Pods out of rotation; disabling it makes rollouts worse."}],"answer":1}],"exercise":{"kind":"choice","multiple":false,"options":["startup","initial-delay","no-liveness","liveness-readyz"],"correct":["startup"],"title":"The restart loop on big databases","prompt":"<p>After a customer’s database grew, notes-api’s startup cache warm-up takes up to 90 seconds. Its liveness probe on <code dir=\"ltr\">/healthz</code> uses the defaults (<code dir=\"ltr\">periodSeconds: 10</code>, <code dir=\"ltr\">failureThreshold: 3</code>), so Pods are killed at about 30 seconds and never finish starting. The <code dir=\"ltr\">/healthz</code> handler isn’t registered until warm-up completes. Which change is the best fix?</p>\n","hints":["You want generous patience during startup and quick detection of hangs afterwards. Which probe separates those two phases?","Count: how long does <code dir=\"ltr\">periodSeconds × failureThreshold</code> need to be?"],"explanation":"<p>A startup probe holds off liveness and readiness until it succeeds once. With <code dir=\"ltr\">periodSeconds: 5</code> and <code dir=\"ltr\">failureThreshold: 24</code>, notes-api gets up to 120 seconds to warm up, is detected as started within 5 seconds of finishing, and from then on liveness runs at its normal 10-second pace:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">startupProbe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  httpGet</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/healthz</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    port</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">http</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  periodSeconds</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  failureThreshold</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</span></span></code></pre></div>\n<p><code dir=\"ltr\">initialDelaySeconds</code> would work today but delays every start by the worst case and breaks again when warm-up hits 95 seconds.</p>\n","checkLabels":{},"optionText":{"startup":{"text":"Add a <code dir=\"ltr\">startupProbe</code> on <code dir=\"ltr\">/healthz</code> with <code dir=\"ltr\">periodSeconds: 5</code> and <code dir=\"ltr\">failureThreshold: 24</code>.","why":"Correct. Up to 120 seconds to start, checked every 5 seconds, then liveness takes over unchanged."},"initial-delay":{"text":"Set <code dir=\"ltr\">initialDelaySeconds: 90</code> on the liveness probe.","why":"It delays liveness by 90 seconds on every start, even fast ones, and fails again as soon as warm-up exceeds 90 seconds."},"no-liveness":{"text":"Remove the liveness probe; readiness is enough.","why":"Readiness never restarts anything, so a deadlocked process would sit there forever, out of rotation but never fixed."},"liveness-readyz":{"text":"Point the liveness probe at <code dir=\"ltr\">/readyz</code> instead.","why":"That doesn’t add startup time, and it ties restarts to database health, which turns database blips into outages."}}},"words":709,"readMinutes":3}