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
Two timelines. Start: PID 1 starts at t=2 and listens at t=4 while the postStart hook copies the config from t=2 to t=5; the container status is Waiting (ContainerCreating) until t=5, then Running. Stop: the pod is deleted at t=10, the 30 second grace period starts, preStop sleeps 5 seconds, SIGTERM is sent at t=15 and the app shuts down; SIGKILL would come at t=40
postStart runs beside the ENTRYPOINT and only holds back the Running status; preStop runs first on delete and its time comes out of the grace period.

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

HandlerWhere it runsCounts as failed when
execa new process inside the container (the kubelet calls CRI ExecSync); it uses the container's CPU and memorythe command exits non-zero
httpGetthe kubelet itself sends an HTTP GET to the pod IP and portthe status is not 2xx, or the connection fails
sleepnowhere: 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:

  1. runs the preStop hook and waits for it;
  2. calls StopContainer with the time that is left; containerd sends SIGTERM to PID 1;
  3. 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.

Two rows from t=10 to t=16. In both, kube-proxy keeps routing to the pod until t=13. Without preStop the app closes its listener at t=10 and the requests at t=10.5, 11.5 and 12.5 are refused. With preStop sleep 5 the listener stays open until SIGTERM at t=15 and the same three requests are served
Routers learn about the delete 3 s late; a preStop sleep keeps the listener open long enough to serve what still arrives.
Pod specSIGTERMkube-proxy stops routingRequests lost
no preStop (Demo 5)t=10, the app closes its listenert=133 refused (t=10.5, 11.5, 12.5)
preStop: sleep 5 (Demo 4)t=15t=130: the app still served them
preStop fails at once (Demo 7)t=11t=132 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 stopspreStop?
pod deleted through the API (kubectl delete, rolling update, scale down)yes
liveness or startup probe failedyes (Demo 8, second half)
preemption, eviction for node pressure, drainyes
postStart failedyes, 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 kernelno: 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 pod or kubectl get events (FailedPostStartHook, FailedPreStopHook). Have an exec hook write to /proc/1/fd/1 if 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 ContainerCreating with all images pulled is often a postStart that never returns.
  • A pod that takes exactly terminationGracePeriodSeconds to 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.