Health Probes and Container Lifecycle
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
A Pod that is merely running is not the same thing as a Pod that is working, or one that is ready to be sent traffic. This page covers the mechanism Kubernetes uses to tell the three apart — probes — and the mechanism it uses to shut a container down cleanly instead of abruptly — lifecycle hooks and graceful termination. It is written against Kubernetes 1.37 (latest patch 1.37.1); see the section index for the full version baseline. It assumes the Pod vocabulary introduced in Pods and builds directly on it: everything here happens to containers inside an already-scheduled Pod.
Liveness, readiness and startup probes
The kubelet can run three kinds of probe against a container, and each answers a different question:
| Probe | Question it answers | On repeated failure |
|---|---|---|
|
"Is this container’s process still healthy, or has it wedged?" |
The kubelet kills the container, and it is restarted according to the Pod’s |
|
"Is this container ready to receive traffic right now?" |
The Pod’s IP is removed from every Service’s EndpointSlice; the container keeps running, it just stops receiving new requests until it reports ready again. |
|
"Has this container finished starting up yet?" |
While a startup probe is defined and has not yet succeeded, the kubelet does not run the liveness or
readiness probes at all; if the startup probe itself keeps failing past its own |
A container can define all three at once, and normally should for anything with a non-trivial startup: the startup probe covers the slow, one-time initialization window; the liveness and readiness probes then run with short, cheap intervals for the rest of the container’s life.
The four check mechanisms
Every probe (whichever of the three kinds it is) defines exactly one handler, one of:
| Mechanism | How success is determined |
|---|---|
|
The kubelet performs an HTTP |
|
The kubelet attempts a TCP connection to |
|
The kubelet runs |
|
The kubelet calls the standard gRPC Health
Checking Protocol |
All four mechanisms, and all three probe kinds, are generally available; none of them requires a feature gate on a current cluster.
Timing fields
Every probe shares the same five timing fields, regardless of its kind or mechanism:
| Field | Default | Meaning |
|---|---|---|
|
|
Seconds to wait after the container starts before running the probe for the first time. |
|
|
How often to repeat the probe. |
|
|
How long to wait for the probe itself to respond before counting that attempt as a failure. |
|
|
Consecutive successes needed, after a failure, before the container is considered healthy/ready again. |
|
|
Consecutive failures needed before the probe is considered to have failed and the action in the table above is taken. |
A probe can also set its own terminationGracePeriodSeconds, which overrides the Pod-level value from
[termination] specifically for the container kill that a failed liveness or startup probe triggers — letting a
slow-to-clean-up container get a longer (or, for a probe that should fail fast, shorter) grace period than an
ordinary Pod deletion gets.
containers:
- name: catalog-api
image: ghcr.io/example/bookshelf-catalog-api:1.0.0
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 30 # up to 30 * 5s = 150s to finish starting
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
failureThreshold: 2
Pitfalls
-
A liveness probe that checks a dependency. A liveness probe should only ask "is this process still able to make progress", never "is my database reachable". If
catalog-api’s liveness probe fails whenever PostgreSQL is briefly unavailable, every `catalog-apiPod gets restarted at once — which does nothing to fix the database, removes every healthy instance’s warm caches and connections simultaneously, and can turn one dependency outage into a self-inflicted cascading failure across the whole Deployment. That check belongs in the readiness probe instead: a Pod that cannot reach its database should stop receiving traffic, not be killed. -
A slow starter with no startup probe. If a container can legitimately take, say, 90 seconds to warm up, and the only probe defined is a liveness probe with
periodSeconds: 10andfailureThreshold: 3(fails at 30 seconds), the kubelet kills a perfectly healthy, still-starting container in a loop, forever. RaisinginitialDelaySecondson the liveness probe to 90 "fixes" that case but then leaves a genuinely wedged process undetected for 90 seconds on every restart. AstartupProbeis the correct fix: it owns the slow, variable startup window, and the liveness probe can then keep its normal, short interval for the rest of the container’s life. -
Too aggressive timing under load. A
timeoutSecondsset for the happy path can start failing the moment the application is under real load (garbage collection pauses, a busy thread pool) rather than actually unhealthy, which is the same cascading-restart problem as the first bullet with a different trigger.
History: Book 1’s pre-v1.0 probes
Book 1 (2015) describes health checks as they existed just before Kubernetes 1.0, and two things have changed since:
-
The book documents only a liveness check (a "process check", plus TCP, HTTP and
exechandlers driving container restarts); a separate readiness probe, controlling only Service membership rather than restarts, did not exist yet at the time it describes and was added shortly afterwards. ThestartupProbekind is newer still (stable since Kubernetes 1.20). -
The book describes reaching application-specific health beyond a fixed set of mechanisms through a custom "prober" process that inspects the application and relabels Pods to add or remove them from a Service’s selector. That idea is exactly what the
execandgrpcmechanisms above make unnecessary: a probe can now run arbitrary application logic (a shell command, or a gRPC health check) directly as part of the readiness probe itself, with the kubelet doing the relabelling-equivalent work (removing the Pod from EndpointSlices) natively, rather than a bespoke side process editing labels.
Lifecycle hooks and graceful termination
postStart and preStop hooks
Besides probes, a container can define two lifecycle hooks, each with the same exec, httpGet or sleep
handler choices described in [lifecycle-hooks-table]:
| Hook | When it runs |
|---|---|
|
Immediately after the container is created, concurrently with the container’s own entrypoint — there is no
guarantee the application has finished starting, or even started at all, when |
|
Immediately before the container is sent |
Both hooks accept a sleep action — a plain fixed pause, with no command or HTTP call — alongside exec and
httpGet:
lifecycle:
preStop:
sleep:
seconds: 5
A sleep preStop hook is the simplest possible way to buy time between a Pod being marked for deletion and the
application receiving SIGTERM, without writing or shipping any script — see [termination] for why that gap
matters. Hook delivery is at least once: a hook can, in rare cases (for example a kubelet restart while
delivering it), run more than once, so a preStop/postStart handler should be safe to run twice. If a hook
fails, the container is killed as a FailedPostStartHook / FailedPreStopHook Pod event.
Source: Container Lifecycle Hooks
Graceful termination
Deleting a Pod is not an instant kill. [termination-sequence] shows one step of a rolling update: the replacement Pod must pass its readiness probe and join the EndpointSlices before an old Pod is deleted, and the old Pod then goes through graceful termination. The two timelines that run in parallel at the start of that termination are the part most often gotten wrong:
-
Endpoint removal and the preStop hook run in parallel, not one after the other. Endpoint removal has to propagate through the EndpointSlice controller, to every node’s kube-proxy, before traffic actually stops arriving — and that propagation is not instant. This is the endpoint-removal race: if
SIGTERMwere sent the moment the Pod is markedTerminating, the application could shut down before some nodes have finished removing it from their load-balancing rules, and a request could be routed to a Pod that is no longer there to answer it. ApreStophook that simply sleeps for a few seconds (thesleepaction above) is the standard mitigation: it delaysSIGTERM, and therefore delays the application actually stopping, long enough for endpoint removal to have finished everywhere, without the application needing to know anything about EndpointSlices at all. -
terminationGracePeriodSeconds(Pod-level, default 30 seconds) is the total budget: it covers thepreStophook and the time the application takes to shut down afterSIGTERM, combined. ApreStophook that sleeps for 10 seconds and an application that then takes 25 seconds to drain will together exceed a 30-second grace period and getSIGKILL`ed mid-shutdown — raise `terminationGracePeriodSecondson the Pod spec (not just the probe-level override from Liveness, readiness and startup probes) if the two combined need more room. -
SIGKILLafter the grace period elapses is unconditional and gives the process no further chance to clean up; a container that needs more time than that should get a longer grace period, not rely on catchingSIGKILL(which cannot be caught at all).
spec:
terminationGracePeriodSeconds: 45
containers:
- name: catalog-api
image: ghcr.io/example/bookshelf-catalog-api:1.0.0
lifecycle:
preStop:
sleep:
seconds: 10
Source: Pod Lifecycle (Termination of Pods)
Readiness gates
A readinessProbe is not always the whole story: sometimes a Pod’s readiness genuinely depends on a condition
an external controller determines, not something the container itself can probe (for example, a load-balancer
controller confirming the Pod has actually been registered as a healthy backend). spec.readinessGates names one
or more extra condition types that must also be True before the Pod is considered ready, alongside the built-in
Ready condition and any readinessProbe:
spec:
readinessGates:
- conditionType: "www.example.com/load-balancer-registered"
An external controller sets that condition on the Pod’s status.conditions once it has confirmed whatever it is
responsible for; until it does, the Pod counts as not ready even if every container’s own readiness probe is
passing. This is the same mechanism Gateway API implementations and some ingress controllers use to make sure a
Pod only receives traffic once it is actually wired into the data plane, not just once the application says it is
ready.
Source: Pod Lifecycle (Pod readiness gate)
Application-side probe endpoints
Probes only work if the application exposes something meaningful to check against. Two examples, one per runtime already used elsewhere on this site:
Spring Boot Actuator
Spring Boot’s Actuator module ties directly into the liveness/readiness distinction above through its health
groups, once management.endpoint.health.probes.enabled is turned on:
management:
endpoint:
health:
probes:
enabled: true
group:
readiness:
include: readinessState,db # opt the DataSource health indicator into readiness
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
With that in place, /actuator/health/liveness reports the application’s own liveness state (a deadlock or a
broken internal loop), separately from /actuator/health/readiness. By default each group contains only its own
state indicator (livenessState or readinessState); external HealthIndicator checks — a database connection
pool, a message broker, a downstream client — are not included. Add them to readiness explicitly with
management.endpoint.health.group.readiness.include, as the db entry above does, and never add them to the
liveness group: a database outage would then restart every replica. The Pod spec then simply points each
probe at its matching endpoint, as in the startupProbe/livenessProbe/readinessProbe example earlier on this
page. See Metrics and Observability for how the broader
/actuator family (metrics, tracing, info) is configured and secured alongside these two endpoints.
Source: Spring Boot — Kubernetes Probes
FastAPI
A minimal FastAPI service can expose the same two endpoints directly (a startupProbe can reuse
/healthz/live), and use a lifespan handler for its startup and shutdown work:
from contextlib import asynccontextmanager
from fastapi import FastAPI, Response
state = {"ready": False}
@asynccontextmanager
async def lifespan(app: FastAPI):
# startup: open connection pools, warm caches, ...
state["ready"] = True
yield
# shutdown: runs after Uvicorn has finished the in-flight requests
state["ready"] = False
# close connection pools, flush buffers, ...
app = FastAPI(lifespan=lifespan)
@app.get("/healthz/live")
def liveness():
return {"status": "ok"}
@app.get("/healthz/ready")
def readiness(response: Response):
if not state["ready"]:
response.status_code = 503
return {"status": "starting"}
return {"status": "ok"}
The application does not install its own SIGTERM handler. Uvicorn already handles SIGTERM gracefully: it
stops accepting new connections, waits for in-flight requests to finish, runs the lifespan shutdown code and
exits. Replacing that handler with signal.signal(signal.SIGTERM, …) would stop Uvicorn from ever shutting
down, and the container would be SIGKILL`ed when the grace period from [termination] runs out. Stopping new
traffic is Kubernetes' job: the terminating Pod is removed from the EndpointSlices, and the `preStop sleep gives
that removal time to finish before SIGTERM arrives.