You run kubectl delete pod, and the pod sits in Terminating. You count. It disappears at almost exactly 30 seconds, every time.
Thirty seconds is Kubernetes' default terminationGracePeriodSeconds. If your pod reliably takes the whole thing, it almost always means one thing: your app never received SIGTERM, or ignored it, and the kubelet had to SIGKILL it.
SIGKILL means in-flight requests got cut off mid-response, open transactions were abandoned, and nothing got flushed. Every deploy, every node drain, every scale-down does this to you.
What's supposed to happen
kubectl delete pod │ ▼ kubelet runs preStop (if any) │ ▼ SIGTERM ──► PID 1 in each container │ │ │ └─ app catches it, finishes in-flight work, exits 0 │ ▼ (only if still running when the grace period ends) SIGKILL ──► everything, no cleanup The signal goes to PID 1 in the container, which may not be your app.
Cause 1: PID 1 is a shell, not your app
The shell form of CMD or ENTRYPOINT wraps your command in /bin/sh -c:
# shell form: PID 1 is /bin/sh, your app is its child CMD npm start PID 1 /bin/sh -c npm start PID 7 node /app/node_modules/.bin/npm start PID 19 node server.js SIGTERM goes to /bin/sh, which (unless it execs the command) doesn't pass it on to its children. Your server never hears it. Thirty seconds later, SIGKILL.
(npm start adds another process to the chain, and how npm passes signals along has changed between versions. Running node directly removes the question.)
Cause 2: your app is PID 1 and has no handler
This one's sneakier. Say you fixed cause 1:
CMD ["python", "server.py"] Now Python is PID 1. But PID 1 is special in Linux: the kernel doesn't apply the default action for signals that PID 1 hasn't installed a handler for. A normal process that gets SIGTERM with no handler dies. PID 1 with no handler just ignores it.
So an app that would shut down fine on your laptop ignores SIGTERM inside a container, and waits for the SIGKILL.
The fixes
1. Use the exec form, and run the binary directly.
CMD ["node", "server.js"] 2. Handle SIGTERM explicitly. Stop accepting new work, finish what's in flight, then exit.
// Node.js process.on('SIGTERM', () => { server.close(() => process.exit(0)); // stop accepting, finish in-flight }); # Python import signal, sys def shutdown(signum, frame): # finish in-flight work here sys.exit(0) signal.signal(signal.SIGTERM, shutdown) // Go ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 25*time.Second) defer cancel() srv.Shutdown(shutdownCtx) 3. Or put a tiny init in front of it. tini and dumb-init run as PID 1, forward signals to your app, and reap zombie processes. Docker has it built in with docker run --init, but Kubernetes doesn't, so install it in the image (the path varies by distro):
ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["node", "server.js"] Check yours in 10 seconds
kubectl exec <pod> -- cat /proc/1/cmdline | tr '\0' ' '; echo If that prints /bin/sh -c ... or npm ..., you've found your 30 seconds.
Then time a delete:
time kubectl delete pod <pod> A pod that handles SIGTERM usually goes in a second or two. One that hits ~30s is being killed.
One more gotcha: exiting too fast
Once SIGTERM works, there's an opposite problem. Kubernetes removes the pod from Service endpoints at the same time it sends SIGTERM, and that removal takes a moment to reach every node and load balancer. An app that exits instantly can drop requests that were already routed to it. The usual fix is a short preStop sleep so the pod keeps serving while traffic drains away. I wrote up the full drain and termination timeline with diagrams if you want the whole chain.
Your turn
What was the cause in your case? Shell form, npm start, a missing handler, or something weirder? And has anyone actually measured how many requests they were losing per deploy before fixing it?


