Implement graceful shutdown patterns that let your services restart, deploy, and scale without dropping in-flight requests. Covers signal handling, connection draining, health check coordination, Kubernetes preStop hooks, and timeout strategies for zero-downtime deployments.
Graceful shutdown is the practice of stopping a service so that in‑flight work completes before the process exits. It is required whenever a host is removed from a cluster, a deployment rolls forward, a node is scaled down, or infrastructure is reclaimed. Without a graceful shutdown procedure, a load balancer or client can finish the current request and then immediately receive an error, a database transaction can be rolled back mid‑statement, or a connection pool can leave sockets in a half‑closed state. This page provides concrete steps, commands, and code patterns that let a service drain connections, let outstanding work finish, and only then exit.
The first step is sending the right signal and giving the process enough time to respond. Three common entry points are the operating system service manager, Docker, and Kubernetes.
systemctl stop <service> sends SIGTERM to the main PID and waits according to TimeoutSec in the service unit (default 90 s). If the process does not exit, systemd sends SIGKILL. To customize the wait, edit the unit: TimeoutSec=120.docker stop --time 30 <container_id> sends SIGTERM and waits up to 30 seconds. The --time flag overrides the container’s STOPTIMEOUT label. A value of 0 sends SIGTERM and immediately follows with SIGKILL.kubectl drain <node> --ignore-daemonsets --delete-emptydir-data --timeout 5m marks the node unschedulable, removes it from the load balancer after the grace period, and waits up to 5 minutes for pods to terminate. Under the hood, each pod receives SIGTERM after its terminationGracePeriodSeconds (default 30 s) elapses. If the pod has a preStop hook, it runs after SIGTERM and before the process exits.A bare kill -SIGTERM <pid> is the universal equivalent; it triggers the same signal‑handling path as the tools above. The critical invariant is that SIGTERM is a request to shut down gracefully, whereas SIGKILL forcibly terminates the process, bypassing all drain logic.
Once the signal is delivered, the service must stop admitting new connections while completing those already in progress. The pattern is language‑agnostic: set a flag in a signal handler, break the accept/read loop, drain pools, then exit.
Python example (standard library only)
import signal, time, sys
draining = False
def _sigterm(signum, frame):
global draining
draining = True
signal.signal(signal.SIGTERM, _sigterm)
while not draining:
conn, addr = sock.accept()
# honour the flag inside the handler
handle(conn, draining)
# after the loop: finish pending work, close listener
sock.close()
# allow remaining checked‑out connections to return/finish
The key behaviours are:
Other runtimes have analogous patterns. Node.js uses process.on('SIGTERM'), Java services register a ShutdownHook, and Go programs use os/signal to catch os.Interrupt/os.Kill. The mechanics are the same: stop accepting, let in‑flight finish, exit.
The infrastructure layer must stop routing new traffic to the host while it drains. The orchestrator typically handles this, but the behaviour depends on configuration.
kubectl drain sets the node’s unschedulable condition to true and removes the node from all endpoint slices or services. After the grace period, the control plane withdraws the node’s IP from the cluster‑wide load balancer. New pods are not scheduled onto the node; existing pods receive SIGTERM and drain as described above.aws elb deregister-instances-from-load-balancer --instances <id> (classic ELB) or gcloud compute backend-services remove-backend <service> --region <region> --balancer <balancer>. The pattern is always the same: mark the host as draining, wait for in‑flight requests to complete, then remove it.If the load balancer continues sending requests to a host that has already signalled shutdown, clients receive 503 Service Unavailable or connection reset by peer, depending on whether the server closed the socket or merely stopped accepting.
Application‑level connection pools must also observe the drain flag. The generic pattern is:
PostgreSQL illustration (using pgbouncer in transaction mode, concepts apply to any pool):
drain_mode = true in pgbouncer. New client connections are rejected with 53300 Client is temporarily disabled.pgbouncer then closes the underlying server connection.Address already in use unless SO_REUSEADDR or SO_REUSEPORT is configured.Common failure mode: a process exits while the pool still holds checked‑out connections. The orchestrator kills the pod (SIGKILL), the pool is abandoned, and the database logs could not receive data from client. The client receives connection reset by peer. To avoid this, always ensure the application’s signal handler sets the drain flag *before* the orchestrator’s grace period expires.
After the signal, drain, and exit, confirm that the host is fully out of the loop.
ss -tlnp | grep <port> should show no LISTening state on the drained node. If a port remains open, either the process didn’t exit or SO_REUSEADDR allowed a new instance to bind prematurely.kubectl get nodes should list the node as Ready (or NotReady if you prefer to see it drained) and its endpoints should be empty. Cloud‑provider GUIs typically show the instance removed from the target group.curl -s -o /dev/null -w "%{http_code}" http://<drained-ip>:<port>/health should return 000 (connection refused) or 503 (explicitly returned by the server’s shutdown handler). A 200 means the server is still accepting traffic."Graceful shutdown complete" or "All connections drained, exiting". Absence of such a line after the grace period is a strong indicator that the drain logic was bypassed.Even with the pattern above, production outages occur when these edge cases are overlooked.
terminationGracePeriodSeconds is too short, the orchestrator sends SIGKILL mid‑drain. In‑flight requests are dropped immediately; clients see 503 or connection reset. Always set the grace period at least 2× the longest expected request latency for your workload.idle in transaction sessions: a connection that checked out a transaction and never committed/rolled back will hold a database slot indefinitely. On shutdown, explicitly ROLLBACK any in‑flight transactions rather than relying on the database to clean them up on disconnect.EADDRINUSE. Align the pool’s drain timeout with the orchestrator’s terminationGracePeriodSeconds.SO_REUSEADDR vs SO_REUSEPORT: binding with SO_REUSEADDR allows a new process to take over the port as soon as the old one closes, which can cause request loss or errors if the old process hasn’t fully released socket state. SO_REUSEPORT (Linux) allows multiple processes to bind the same port if they all set the flag, but requests are distributed among them—useful for rolling restarts, but requires application‑level sharding logic.Understanding these edges before they manifest in production is far less costly than debugging dropped requests, timed‑out connections, or database errors after a shutdown has already begun.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy