The idea: two hooks the kubelet runs for you
A container can have two lifecycle hooks in its spec. postStart runs right after the container starts. preStop runs right before Kubernetes stops it. Neither runs inside your program's code. The kubelet on the node runs them, around the moments when it starts and stops the container.
containers:
- name: app
image: web:2.1
lifecycle:
postStart:
exec: {command: ["sh", "-c", "cp /config/* /app/"]}
preStop:
sleep: {seconds: 5}
terminationGracePeriodSeconds: 30
The canvas shows one pod on one node. At the top: the control plane (the pod object, its events and the EndpointSlice), the node (kubelet and containerd) and the pod with its containers. Each container has a hook bar, orange while a hook runs. Below them, a client sends a request every second, and kube-proxy sends some of them to this pod. At the bottom a timeline lines up the container states, both hooks, the grace period, the endpoint, kube-proxy and every request: ✓ served, ✗ refused, ⏱ timed out.
Three kinds of handler
| Handler | Where it runs | Counts as failed when |
|---|---|---|
exec | a new process inside the container (the kubelet calls CRI ExecSync); it uses the container's CPU and memory | the command exits non-zero |
httpGet | the kubelet itself sends an HTTP GET to the pod IP and port | the status is not 2xx, or the connection fails |
sleep | nowhere: the kubelet just waits (no sleep binary needed in the image) | never |
The sleep action is newer (beta and on by default since Kubernetes v1.30). Before it, people wrote
exec: ["sleep", "5"], which fails on distroless images that have no shell and no sleep.
postStart runs beside the ENTRYPOINT
The kubelet asks containerd to start the container, so PID 1 (the ENTRYPOINT) is already running.
Then it runs postStart. Nothing guarantees that the hook runs before your program does anything. The two run side by side.
What the hook does block is the container's status: the container stays Waiting (ContainerCreating), and the
pod stays Pending, until the hook returns. The probes do not start before that either (Demo 1: PID 1
starts at t=2, the app listens at t=4, but the container is Running only at t=5).
The kubelet also starts the containers of a pod one after the other, and it waits for one container's
postStart before it starts the next. Service meshes use this: put the proxy first with a postStart that waits until the proxy
is ready, and the app never starts without its network. Istio does exactly this with
holdApplicationUntilProxyStarts (Demo 2: the app container starts at t=6, not at t=2).
If postStart fails, the kubelet records the event FailedPostStartHook and kills the container. Then it treats
it like any other dead container: with restartPolicy: Always it restarts it after a back-off of 10 s, 20 s, 40 s and
so on, and the pod shows CrashLoopBackOff. postStart runs again on every restart (Demo 3).
preStop runs before SIGTERM, inside the grace period
When you delete a pod, the API server sets deletionTimestamp, and the grace period
(terminationGracePeriodSeconds, default 30 s) starts counting at once. The kubelet then:
- runs the preStop hook and waits for it;
- calls
StopContainerwith the time that is left; containerd sends SIGTERM to PID 1; - if PID 1 is still alive when the time is up, sends SIGKILL (exit code 137 = 128 + 9).
The hook's time is part of the grace period, not added to it. A 5 s preStop leaves 25 s for the app to
shut down (Demo 4). If preStop is still running when the grace period ends, the kubelet stops waiting. It sends SIGTERM
and gives a one-off 2 s extension, then SIGKILL (Demo 6: SIGTERM at t=40, SIGKILL at t=42).
If a long preStop is what you want, raise terminationGracePeriodSeconds as well.
A preStop that fails does not stop the shutdown. The kubelet records FailedPreStopHook and sends SIGTERM
right away (Demo 7).
The endpoint race, and why sleep 5 works
Deleting a pod starts two things at the same time. The kubelet starts shutting the container down. The
EndpointSlice controller marks the pod's endpoint terminating. But kube-proxy on every node, and any Ingress or
cloud load balancer, learns about that change a little later. In this model that takes 3 s. Until then they keep sending new
requests to the pod.
| Pod spec | SIGTERM | kube-proxy stops routing | Requests lost |
|---|---|---|---|
| no preStop (Demo 5) | t=10, the app closes its listener | t=13 | 3 refused (t=10.5, 11.5, 12.5) |
preStop: sleep 5 (Demo 4) | t=15 | t=13 | 0: the app still served them |
| preStop fails at once (Demo 7) | t=11 | t=13 | 2 refused |
So a short preStop sleep is not a hack. It keeps the app serving until the routers have caught up. The best fix is in the app:
keep serving for a few seconds after SIGTERM before closing the listener. The sleep does the same for apps you cannot change.
Some servers need a different signal to shut down gently. Nginx treats SIGTERM as "quit now", so people use
preStop: exec ["nginx", "-s", "quit"] (SIGQUIT, graceful) instead.
When preStop runs, and when it does not
| Why the container stops | preStop? |
|---|---|
pod deleted through the API (kubectl delete, rolling update, scale down) | yes |
| liveness or startup probe failed | yes (Demo 8, second half) |
| preemption, eviction for node pressure, drain | yes |
| postStart failed | yes, if the container has one |
the process exited by itself (crash, exit 0, Job done) | no: there is nothing left to stop (Demo 8, first half) |
| OOM kill by the kernel | no: the kernel kills it, not the kubelet |
In Demo 8 the app first crashes: exit 1, no preStop, restart after 10 s. Then it hangs at t=28. The next probes, at t=33, fail: readiness takes it out of the EndpointSlice, but kube-proxy keeps sending it requests until t=36, and all 8 of them time out. After three liveness failures the kubelet kills it, and this time preStop runs before SIGTERM.
Debugging hooks
- The hook's output is not in
kubectl logs. A failure shows only as an event:kubectl describe podorkubectl get events(FailedPostStartHook,FailedPreStopHook). Have anexechook write to/proc/1/fd/1if you need its output in the container log. - Hooks are delivered at least once. A kubelet restart in the middle can run one again, so make them idempotent.
- A pod stuck in
ContainerCreatingwith all images pulled is often a postStart that never returns. - A pod that takes exactly
terminationGracePeriodSecondsto go away is often a preStop that never returns, or an app that ignores SIGTERM.
What the page leaves out
One node and one replica are drawn; the other replica only appears as a box in the traffic row. Native sidecars
(init containers with restartPolicy: Always, Kubernetes v1.29+) have their own start and stop order: they start
before the app containers and stop after them. The page uses an ordinary sidecar container. The deprecated
tcpSocket handler, Windows containers, hook timeouts in the container runtime and the full pod start
(scheduler, image pulls, init containers) are not shown. For the whole life of a pod, see
The Life of a Kubernetes Pod. For what kube-proxy does with the EndpointSlice, see
The Life of an HTTP Request in Kubernetes.