Operating systems and concurrency
Signals and graceful shutdown
How the kernel tells a process to stop, why SIGTERM can be caught and SIGKILL cannot, and how a service drains work before it exits. Covers the PID 1 trap in containers.
A signal is a short notification the kernel delivers to a process, such as "stop now" or "a child exited". Signals are how the outside world tells your service to shut down. If your service handles them well, a deploy finishes its in-flight requests. If it doesn't, those requests fail halfway through.
The big idea
The signal you care about most is SIGTERM (number 15). It is a polite request to stop. Your program can catch it, finish its work and exit. SIGKILL (number 9) is different. The kernel destroys the process, and your program never sees the signal. There is no cleanup, no flushing and no "finish the current request".
| Signal | Number | Default action | Typical source |
|---|---|---|---|
| SIGINT | 2 | Terminate | Ctrl-C in a terminal |
| SIGKILL | 9 | Terminate, cannot be caught | kill -9, the OOM killer, a runtime after its grace period |
| SIGTERM | 15 | Terminate, can be caught | kill, docker stop, Kubernetes, systemd |
| SIGHUP | 1 | Terminate | Terminal closed; many daemons reload config on it |
| SIGCHLD | 17 | Ignore | A child process exited |
Concept
Default action: what the kernel does when no handler is installed. For most signals that is "terminate". A handler is a function your program registers to run when the signal arrives.
Walking through a Kubernetes shutdown
When Kubernetes deletes a pod, the steps run roughly in this order.
Removing the endpoint and sending SIGTERM happen at about the same time. A few requests may still arrive just after SIGTERM. Many teams add a short preStop sleep of 5 to 10 seconds so load balancers catch up before the app stops listening.
Here is the shape of a graceful shutdown in Go. This is a sketch: mux and db come from your app.
srv := &http.Server{Addr: ":8080", Handler: mux}
go srv.ListenAndServe()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
<-stop
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
srv.Shutdown(ctx) // stop listening, wait for in-flight requests
db.Close()The 25-second timeout is shorter than Kubernetes' 30-second grace period. The process then exits on its own terms, not because SIGKILL arrived.
The PID 1 trap in containers
Inside a container, your entrypoint is PID 1. The kernel treats PID 1 specially. Signals whose default action is "terminate" are ignored unless the process has installed a handler.
So a Node or Python script that never registers a SIGTERM handler ignores docker stop. Docker waits 10 seconds, then sends SIGKILL. Every deploy then takes 10 extra seconds per container, and in-flight work is cut off.
Two other mistakes cause the same trouble.
- Shell-form entrypoints.
CMD npm startruns/bin/sh -c "npm start". The shell is PID 1. It may not forward SIGTERM to your app. Use exec form,CMD ["node", "server.js"], orexecin a wrapper script. - No reaper. If your app spawns child processes and PID 1 never calls
wait(), zombies pile up. Use a small init such astini(docker run --initadds one).
Common mistakes
- Ignoring SIGTERM. The process gets killed mid-request after the grace period. Install a handler that drains and exits.
- Finishing old work while still taking new work. Stop accepting new requests first, then finish what is in flight.
- Setting a shutdown timeout longer than the grace period. The kernel's SIGKILL wins, and your cleanup never runs.
Check yourself
- Why can't a program clean up after SIGKILL? The kernel destroys the process directly. The program never gets to run handler code.
- Your container takes 10 seconds to stop on every deploy. What is the likely cause? PID 1 ignores SIGTERM because no handler is installed, so Docker waits for the timeout and then sends SIGKILL.
Key takeaways
- SIGTERM is a request your program can catch. SIGKILL is not delivered to your program at all.
- A graceful shutdown stops accepting work, finishes in-flight requests, closes resources and exits before the grace period.
- In containers, PID 1 ignores SIGTERM unless a handler is installed. Use exec-form
CMDand a handler. - A process that stops late is usually waiting for the kernel's SIGKILL. Measure how long shutdown takes.
Further learning
The Kubernetes docs cover the exact pod termination order. The signal(7) man page lists every signal and its default action.
signal(7): overview of Linux signalssignal(7) - Linux manual pageman7.orgRelated
Processes, threads and the kernel
What the OS gives every program, and how to see it on a live Linux box. Start here for the process model.
Process lifecycle
How a process is born with fork, takes on a new program with exec, and exits with a code. Why zombies build up when a parent never calls wait.
Linux for engineers
The parts of Linux you touch in production, from shell pipelines and systemd services to permissions, disks, network debugging commands and safe remote access.