Kubernetes graceful shutdown for Node.js requires the server to stop receiving traffic, finish active requests, and exit before the pod's grace period ends. shutdown-check tests the server-side part locally or in CI; it does not emulate the Kubernetes control plane.
What happens when a pod stops?
During deletion, a rolling update, scale-down, or node drain:
- Kubernetes marks the pod as terminating.
- Endpoint removal begins so Services and load balancers can stop routing to the pod.
- The kubelet runs a configured
preStophook. - The container runtime sends
SIGTERMto the container's main process. - Kubernetes waits for the container to exit.
- At the end of
terminationGracePeriodSeconds, remaining processes receiveSIGKILL.
Endpoint updates and container termination begin around the same time. Traffic can still arrive after the signal because kube-proxy, ingress controllers, and external load balancers need time to observe the change.
What must the service do?
The server should:
- change readiness immediately;
- reject new work that still arrives;
- preserve requests that began before the drain;
- close shared resources after active handlers finish;
- exit with code
0before the grace period ends.
Closing the listener immediately may be enough when every client retries and
traffic routing updates quickly. A safer zero downtime deploy often uses a
short drain window that returns 503 before closing the listener.
Configure readiness and preStop
spec:
template:
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
image: example/api:1.4.2
command: ["node", "dist/server.js"]
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 2
failureThreshold: 1
lifecycle:
preStop:
exec:
command: ["sleep", "5"]The preStop delay gives endpoint updates time to propagate before SIGTERM.
It uses part of the 30-second grace period; it does not add extra time.
The exec form requires a sleep binary in the image. Some Kubernetes versions
offer a built-in sleep action. Confirm support in the cluster before depending
on it.
An application-level drain can replace or supplement preStop. The readiness
route returns 503 as soon as SIGTERM arrives, and the server keeps listening
briefly before calling server.close().
Match Kubernetes to shutdown-check
| Kubernetes setting or behavior | shutdown-check setting |
|---|---|
Container command | command |
readinessProbe.httpGet.path | readiness.path |
| Pod grace period | Upper bound for shutdown.deadlineMs |
preStop duration | Time to subtract from the server's available deadline |
| Requests arriving during termination | shutdown.newRequests |
| Slowest valid application request | workload |
For a 30-second grace period with a 5-second preStop, a 20-second test
deadline leaves about 5 seconds of safety margin:
{
"command": ["node", "dist/server.js"],
"env": { "NODE_ENV": "production", "PORT": "3000" },
"baseUrl": "http://127.0.0.1:3000",
"readiness": { "path": "/health" },
"workload": {
"path": "/test/slow",
"bodyIncludes": "work complete",
"concurrent": 3,
"started": { "type": "response-headers" }
},
"shutdown": {
"deadlineMs": 20000,
"readinessWithdrawal": true,
"newRequests": {
"path": "/test/new-work",
"rejectStatuses": [503]
},
"repeatSignalAfterMs": 1000
}
}shutdown-check begins at SIGTERM, so it does not run or time the preStop
hook. The deadline should describe the time available to the application after
the hook.
Why does PID 1 matter?
The container command becomes PID 1. Signal and child-process behavior around PID 1 is different from an ordinary process, so choose the command carefully.
Use exec form
Preferred Dockerfile command:
CMD ["node", "dist/server.js"]Avoid shell form:
CMD node dist/server.jsShell form starts /bin/sh -c as PID 1. The shell may not forward SIGTERM to
Node.js.
Avoid unnecessary package-manager wrappers
This adds another process between Kubernetes and the server:
CMD ["npm", "start"]Starting node directly makes signal ownership clear. If a wrapper is
required, it must forward signals and wait for the child.
Consider a minimal init process
An init such as tini can forward signals and reap orphaned child processes. It is useful when the application spawns children. Configure it intentionally and test the exact container command used in production.
Diagnose wrapper problems
Use the production command in shutdown-check. The result often reveals the failure before you build an image:
| Symptom | Code |
|---|---|
| Wrapper exits while child server keeps port | SC302 |
| Wrapper is killed by the signal | SC301 |
| Process remains alive through the deadline | SC300 |
| Active request loses its connection | SC201 |
Run shutdown-check inside a container
You can run the tool inside a Linux image when it contains Node.js 22 or later and the package. shutdown-check starts the configured service as a child and connects to it on loopback.
This verifies:
- the built image can start the service;
- its runtime files and environment are present;
- the Node.js process handles
SIGTERM; - active local HTTP requests drain;
- the process and port close.
It does not verify:
- Kubernetes endpoint removal;
- an actual
preStophook; - ingress or external load-balancer timing;
- sidecar termination order;
- cluster-specific grace-period behavior.
Use a staging cluster for those integration checks.
Handle sidecars and service meshes
A sidecar proxy can continue or stop forwarding traffic independently of the Node.js process. shutdown-check talks directly to the local service and does not model sidecar shutdown order.
When using a mesh, confirm:
- readiness reflects both proxy and application state;
- the proxy keeps forwarding existing connections during the app drain;
- sidecar termination does not shorten the application deadline;
- retries do not hide repeated failures.
Set a realistic grace period
Budget the full termination window:
preStop + routing delay + slowest active request + resource cleanup + marginIf the total exceeds terminationGracePeriodSeconds, Kubernetes will send
SIGKILL before the service finishes. Raising the grace period can be valid,
but also fix handlers, queries, or cleanup operations that can hang forever.