EduFlow

Section 5 · Running in Production

Probes and Graceful Shutdown

20 min3 min readChoose wisely

Lesson at a glance

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.

  1. 1Three probes, three consequences
  2. 2Readiness is a traffic switch
  3. 3Startup without restart loops
  4. 4Shutting down without dropping requests

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

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.

Three probes, three consequences

Probe Question it answers When it fails
startup Has the app finished starting? Keep waiting, up to the limit, then restart
readiness Should this Pod receive traffic right now? Remove the Pod from Service endpoints; no restart
liveness Is the process stuck beyond recovery? Restart the container

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.

notes-api already has the two endpoints this needs. /healthz returns ok if the event loop can answer a request at all. /readyz also checks the database pool and returns 503 if Postgres is unreachable.

k8s/deployment.yaml
        - name: api
          image: ghcr.io/skylane/notes-api:1.4.0
          ports:
            - name: http
              containerPort: 3000
          startupProbe:
            httpGet:
              path: /healthz
              port: http
            periodSeconds: 2
            failureThreshold: 30      # up to 60 s to start
          readinessProbe:
            httpGet:
              path: /readyz
              port: http
            periodSeconds: 5
            failureThreshold: 2
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3

Any HTTP status from 200 to 399 is a pass. Besides httpGet there are tcpSocket, grpc and exec probes; prefer HTTP for web services, since it tests the path real traffic takes. The defaults, if you leave fields out, are periodSeconds: 10, timeoutSeconds: 1 and failureThreshold: 3. The one-second timeout catches slow apps by surprise, so set it deliberately.

Readiness is a traffic switch

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 gates rollouts: 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.

Startup without restart loops

Before startup probes existed, slow-starting apps were protected with initialDelaySeconds on liveness. That delays the first check on every start by the worst-case startup time, and breaks again when startup gets slower. A startup probe gives the app periodSeconds × failureThreshold (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.

Shutting down without dropping requests

When a Pod is deleted, during a rollout, a scale-down or a node drain, two things start at the same time: 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.

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. delete Pod endpoints removed preStop sleep 5s SIGTERM: drain, then exit 0 terminationGracePeriodSeconds: 30 SIGKILL
Endpoint removal and termination start together; a preStop pause covers the gap

The fix has two halves. In the Pod spec, pause before SIGTERM so routing catches up:

k8s/deployment.yaml
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: api
          lifecycle:
            preStop:
              sleep:
                seconds: 5

The sleep action runs in the kubelet, so it works even in a distroless image with no sleep 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 terminationGracePeriodSeconds, or the kubelet sends SIGKILL at the deadline.

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.

Key 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 preStop sleep lets traffic drain before the app starts shutting down.
  • The app must finish in-flight work within terminationGracePeriodSeconds (30 by default) or it is killed.

Further reading

Check your understanding

4 questions

Question 1 of 4

notes-api’s readiness probe starts failing on one Pod because it can’t reach Postgres. What does Kubernetes do?

Question 2 of 4

Your liveness probe calls /readyz, which checks the database. Postgres restarts for 20 seconds. What happens?

Question 3 of 4

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?

Question 4 of 4

During every rollout a few requests to notes-api fail with connection refused, even though the app handles SIGTERM. What’s the usual fix?

Practice

Choose wisely

The restart loop on big databases

After a customer’s database grew, notes-api’s startup cache warm-up takes up to 90 seconds. Its liveness probe on /healthz uses the defaults (periodSeconds: 10, failureThreshold: 3), so Pods are killed at about 30 seconds and never finish starting. The /healthz handler isn’t registered until warm-up completes. Which change is the best fix?

The restart loop on big databases