When a pod is deleted, Kubernetes triggers two actions in parallel: sending SIGTERM to the container, and removing the pod from the Service’s endpoint list that references it. These two actions are never synchronized with each other: nothing guarantees endpoint removal propagates before the container starts shutting down, nor the reverse. This lack of ordering guarantee explains most of the 502/503 errors observed during a deployment otherwise considered “zero-downtime.”
Two clocks that don’t talk to each other
The kubelet sends SIGTERM to the container as soon as the pod deletion is processed locally. In parallel, the endpoint controller updates the Service’s EndpointSlice object, an update that then needs to propagate to kube-proxy on every node, then, if a service mesh is involved, to its own routing tables. That second path takes non-zero time, potentially several seconds on a sizable cluster, while the first (SIGTERM) may have already started shutting down the process.
t=0s : pod deletion triggered
t=0s : SIGTERM sent to the container (immediate)
t=0-3s : endpoint removal propagation (variable)
What this actually produces
During the window where endpoint removal hasn’t fully propagated yet, an incoming request can still get routed to the terminating pod: if the application has already reacted to SIGTERM by closing its HTTP server, that request fails with a connection-refused error, exactly the kind of one-off error a “zero-downtime” deployment wasn’t supposed to produce.
preStop: buying the time propagation needs
A preStop hook delays the actual SIGTERM delivery by running an action beforehand, most often a simple sleep: however long it lasts gives endpoints a chance to propagate before the container actually starts shutting down.
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
That sleep fixes nothing on the endpoints side: it simply delays the moment the application stops responding, so it lands after, not before, the removal propagates. The duration to pick depends directly on the propagation time observed in the cluster, not an arbitrary value copied from an online example.
terminationGracePeriodSeconds: the limit past which nothing waits anymore
terminationGracePeriodSeconds sets the total duration (preStop hook included) before Kubernetes sends SIGKILL, an immediate stop with no possible negotiation with the process.
spec:
terminationGracePeriodSeconds: 30
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
A 5-second preStop with a 30-second total grace period leaves 25 seconds for the application itself to properly finish in-flight requests after receiving SIGTERM: a grace period too short relative to actual in-flight request duration forces a SIGKILL that cuts transactions off mid-way, a different problem from the endpoint/SIGTERM race but fixed by the same field.
Takeaway
Kubernetes sends SIGTERM and removes a pod from Service endpoints in parallel, never in a guaranteed order, a race that explains one-off errors observed during an otherwise correctly configured deployment. A preStop hook (often a simple sleep is enough) buys the time endpoint propagation needs, before the application actually stops responding. terminationGracePeriodSeconds sets the absolute limit past which SIGKILL takes over, to size against actual in-flight request duration, not a default value, a detail that separates a genuinely zero-downtime deployment from one that merely claims to be, central for a Kubernetes migration aiming for real service continuity.