Why a gentle stop is not the same as a hard kill
Killing production processes with kill -9 risks corrupted PHP-FPM requests, orphaned locks, and inconsistent data. This article explains how Unix signals really work, why SIGTERM and SIGKILL are fundamentally different, how to trap signals cleanly in your own scripts, and how kill, pkill, killall, and systemd work together for controlled process management.
Table of Contents
- 1. Why signals and process control determine stability
- 2. How signals work: kernel, process table, default actions
- 3. SIGTERM vs. SIGKILL: the critical difference
- 4. Why kill -9 corrupts PHP-FPM in-flight requests
- 5. Graceful shutdown with trap in your own scripts
- 6. kill, pkill, and killall compared
- 7. systemd and containers: TimeoutStopSec, KillSignal, stop_grace_period
- 8. Other important signals: SIGHUP, SIGINT, SIGUSR1/2, SIGSTOP
- 9. Zombie processes, pitfalls, and signal patterns compared
- 10. Summary
- 11. FAQ
1. Why signals and process control determine stability
A Unix signal is an asynchronous notification the kernel sends to a process, informing it about an event or forcing it into a specific reaction. Signals are not messages with payloads like a message queue, but simple numbered interrupts: a process only receives the information that a particular signal has arrived, and then reacts with a registered handler, the kernel's default action, or by ignoring it. For administrators and developers, understanding signals determines whether a process shuts down in a controlled way or gets cut off in the middle of an operation.
In production environments the difference becomes painfully visible: a database dump terminated by SIGKILL leaves behind an incomplete file. A PHP-FPM worker killed in the middle of an order can leave a half-written transaction behind. Anyone who takes process control seriously deliberately distinguishes between gentle signals like SIGTERM, which give a process time to clean up, and hard signals like SIGKILL, which act instantly and without regard for what the process was doing. The following sections show how signals work technically and how to apply them correctly in deployment scripts, PHP-FPM configuration, and systemd units.
2. How signals work: kernel, process table, default actions
The Linux kernel keeps a signal mask and a table of up to 64 standard signals for every process, the first 31 of which are the classic POSIX signals with fixed numbers. Every signal has a default action: terminate, terminate with core dump, ignore, stop, or continue. A process can register its own handler for most signals to override the default action, or explicitly block a signal. Two signals are the exception: SIGKILL (number 9) and SIGSTOP (number 19) can be neither caught, nor ignored, nor blocked, because the kernel enforces them directly at the scheduler level without ever notifying the target process.
The command kill -l lists all available signal names with their numbers and is the fastest way to refresh the mapping. In your own bash scripts, trap -p shows which handlers are currently registered for which signal. These fundamentals are the prerequisite for correctly framing the crucial difference between SIGTERM and SIGKILL in the next section.
# List all standard POSIX signal names with their numbers
kill -l
# Show the signal number for a specific name
kill -l TERM # 15
kill -l KILL # 9
kill -l HUP # 1
# Show currently registered trap handlers in the current shell
trap -p
# Send a signal to a process by PID (default signal is SIGTERM)
kill 4821
# Explicit signal, three equivalent notations
kill -SIGTERM 4821
kill -TERM 4821
kill -15 4821
# Hard kill, enforced by the kernel scheduler, cannot be caught
kill -SIGKILL 4821
kill -9 4821
3. SIGTERM vs. SIGKILL: the critical difference
SIGTERM (signal 15) is a polite request for a process to terminate. The process stays in control: it can register a handler, close open database connections, finish requests already in progress, delete temporary files, and then exit on its own. That is precisely the point of a graceful shutdown. If the process ignores SIGTERM completely, or fails to react within a reasonable window, it simply stays alive and may block a restart or a deployment.
SIGKILL (signal 9), by contrast, shows no leniency at all. The kernel removes the process from the scheduler queue immediately, without giving it any chance to run handlers, close files, or release resources. Open file descriptors get cleaned up by the kernel itself, but everything the process should have done on its own, such as committing a transaction or writing a consistent state, is simply lost. The rule of thumb in practice: always send SIGTERM first, wait a reasonable amount of time, and reserve SIGKILL as the last resort for hung processes that never respond to SIGTERM.
4. Why kill -9 corrupts PHP-FPM in-flight requests
PHP-FPM processes HTTP requests in worker processes managed by a master process. If a worker is killed with SIGKILL in the middle of a request, the running PHP execution stops exactly wherever it happened to be, without a finally block, a destructor, or any registered shutdown handler ever running. During a checkout in Magento that can mean an order gets created in the system but the cart is never emptied, or a payment confirmation never gets saved even though the payment provider has already reported the transaction as successful.
The PHP-FPM master process reacts differently to SIGTERM and SIGQUIT: SIGQUIT triggers a graceful stop, in which running requests are allowed to finish before the workers are terminated. The configuration directive process_control_timeout defines how long a worker gets after receiving a stop signal before the master helps along with SIGKILL. Setting this value too low creates exactly the aborted requests that a graceful shutdown is supposed to prevent.
; /etc/php/8.4/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
; Grace period a worker gets to finish an in-flight request
; after receiving SIGTERM/SIGQUIT, before the master sends SIGKILL
process_control_timeout = 10s
; Let idle workers shut down gracefully instead of a hard restart
pm.process_idle_timeout = 30s
catch_workers_output = yes
decorate_workers_output = no
5. Graceful shutdown with trap in your own scripts
The trap builtin in bash registers a function or a command that runs when a specific signal arrives. For maintenance scripts, worker processes, and deployment automation, trap is the central tool for reacting to SIGTERM and SIGINT in a controlled way instead of letting the process die unprepared. A typical handler function ends the current loop, waits for running background jobs with wait, and only then returns the exit code.
It is important that the handler itself stays fast and robust: a signal handler that hangs because it is waiting on a resource that is no longer reachable turns a clean shutdown into an unbounded hang. That is why the handler should set its own timeouts and, in doubt, abort itself with exit after a defined deadline. Since SIGKILL cannot be caught, trap only ever works for SIGTERM, SIGINT, SIGHUP, and the user-defined signals SIGUSR1 and SIGUSR2, never for SIGKILL or SIGSTOP.
#!/usr/bin/env bash
# graceful-worker.sh: reacts to SIGTERM/SIGINT and finishes the current job first
set -euo pipefail
RUNNING=1
CURRENT_JOB_PID=""
terminate() {
echo "[INFO] SIGTERM received, waiting for current job to finish" >&2
RUNNING=0
if [[ -n "$CURRENT_JOB_PID" ]]; then
wait "$CURRENT_JOB_PID"
fi
echo "[INFO] Graceful shutdown complete" >&2
exit 0
}
trap terminate SIGTERM SIGINT
while [[ "$RUNNING" -eq 1 ]]; do
process_next_job &
CURRENT_JOB_PID=$!
wait "$CURRENT_JOB_PID"
CURRENT_JOB_PID=""
done
6. kill, pkill, and killall compared
kill, pkill, and killall all solve the same task, sending a signal, in different ways. kill addresses a process exclusively by its PID and is therefore the most precise but also the most cumbersome tool, since you first have to look up the PID, for example with pgrep or ps. pkill searches for processes by a name pattern or other criteria such as user or working directory and sends the signal directly to every match, without the detour through an explicit PID.
killall resembles pkill, but by default expects an exact process name rather than a pattern, and is implemented somewhat differently across Unix systems, which makes it a little less portable on Linux servers than pkill. A common trap with pkill and killall: a pattern that is too broad matches more processes than intended, in the worst case even system-critical services with a similar name. The -f flag on pkill matches against the full command line instead of just the process name and should therefore always be combined with as specific a pattern as possible.
# kill: precise, requires a known PID
pgrep -f "php-fpm: pool www" | head -1
kill -TERM 4821
# pkill: match by name pattern, send SIGTERM to every match
pkill -TERM -f "queue:consume magento"
# pkill: only match processes owned by a specific user
pkill -TERM -u www-data -f "bin/magento cron:run"
# killall: exact process name, all instances
killall -TERM php-fpm8.4
# Always send SIGTERM first, wait, then escalate to SIGKILL as a fallback
pkill -TERM -f "worker.php" && sleep 10 && pkill -KILL -f "worker.php"
# Dry-run first: list matches before sending any signal
pgrep -af "worker.php"
7. systemd and containers: TimeoutStopSec, KillSignal, stop_grace_period
systemd sends SIGTERM by default when stopping a service and waits for the time configured in TimeoutStopSec before helping along with SIGKILL if the process has not terminated by then. The KillSignal directive lets you change the initial signal, which makes sense for PHP-FPM, for example, because its master process reacts to SIGQUIT with a true graceful stop, whereas SIGTERM triggers an immediate stop. Setting TimeoutStopSec too short effectively forces a SIGKILL every time, no matter how cleanly the application itself would have responded to SIGTERM.
In container environments the same principle applies under different names: Docker sends the signal configured via stop_signal when stopping a container, SIGTERM by default, and waits stop_grace_period before SIGKILL follows. Kubernetes uses the analogous terminationGracePeriodSeconds. If this value is not adjusted to the actual duration of running requests, the orchestration layer regularly forces hard kills even though the application itself would be perfectly capable of shutting down cleanly if only given enough time.
# docker-compose.yaml: give PHP-FPM enough time to finish in-flight requests
services:
php-fpm:
image: mironsoft/php-fpm:8.4
stop_signal: SIGQUIT # PHP-FPM's own graceful-stop signal
stop_grace_period: 15s # Docker sends SIGKILL after this if still running
healthcheck:
test: ["CMD", "php-fpm-healthcheck"]
interval: 10s
timeout: 3s
retries: 3
8. Other important signals: SIGHUP, SIGINT, SIGUSR1/2, SIGSTOP
Besides SIGTERM and SIGKILL, there are a number of other signals with clearly defined purposes. SIGHUP (signal 1) originally meant the loss of a terminal connection, but today many daemons interpret it as a request to reload their configuration without restarting the process entirely, for example nginx or rsyslog. SIGINT (signal 2) corresponds to pressing Ctrl+C in a terminal and is typically treated, like SIGTERM, as a request to shut down cleanly.
SIGUSR1 and SIGUSR2 are reserved for user-defined purposes and are frequently used to control applications at runtime, for example to reopen log files after rotation or toggle debug output. SIGSTOP and SIGCONT pause and resume a process respectively without terminating it, and like SIGKILL cannot be caught. That is practically useful when debugging a hung process: kill -STOP freezes a process so you can inspect its state without it continuing to run, and kill -CONT resumes it afterward.
{
"timestamp": "2026-07-12T08:14:32Z",
"level": "info",
"pid": 18422,
"signal": "SIGUSR1",
"event": "log_files_reopened",
"message": "Worker received SIGUSR1, reopened log handles after logrotate"
}
9. Zombie processes, pitfalls, and signal patterns compared
A zombie process arises when a child process has terminated but its parent has not yet collected its exit status with wait(). The zombie no longer occupies memory or CPU time, but it does block an entry in the process table, and contrary to popular belief it cannot be terminated with kill, because it is technically already dead. The actual fix lies with the parent process: either it handles SIGCHLD correctly and calls wait(), or it terminates itself, at which point init or systemd adopts and cleans up the orphaned zombies.
Another common pitfall is a signal handler that is itself not async-signal-safe, for example because it allocates memory or runs into a race condition with the main thread. Such handlers can cause deadlocks that only become visible under load. The following overview shows typical signal patterns compared directly between a risky and a robust implementation.
| Task | Risky / Wrong | Recommended Pattern | Benefit |
|---|---|---|---|
| Reloading PHP-FPM | kill -9 $(pgrep php-fpm) |
systemctl reload php8.4-fpm |
Running requests are not aborted |
| Terminating a process | kill -9 <pid> immediately |
kill -TERM, wait, kill -9 as fallback |
Cleanup handlers run to completion |
| Terminating multiple processes | kill -9 $(ps aux | grep name) |
pkill -TERM -f name |
No accidental match on grep itself |
| Making a script signal-safe | No handler, terminates directly | trap cleanup SIGTERM SIGINT |
Resources are guaranteed to be released |
| Stop timeout in systemd | Default value left unchanged | TimeoutStopSec matched to the app |
No hard SIGKILL before the app finishes |
Applying these patterns consistently noticeably reduces aborted requests, zombie processes, and unexplained hangs during deployment. Sending SIGTERM first, waiting a reasonable amount of time, and using SIGKILL only as a fallback is not extra overhead, it is the basic requirement for stable production environments.
Mironsoft
Process management, PHP-FPM tuning, and systemd configuration for Magento servers
Processes that shut down cleanly instead of losing requests?
We analyze your PHP-FPM and systemd configuration, harden signal handling in deployment scripts, and make sure containers and worker processes stop in a controlled way instead of being killed mid-request.
Signal Audit
Analysis of PHP-FPM, systemd units, and deployment scripts for risky kill -9 calls
Graceful Shutdown
trap-based cleanup handlers and matching timeouts for workers and cron jobs
Containers & systemd
Correctly tuning TimeoutStopSec, KillSignal, and stop_grace_period for your workloads
10. Summary
Signals and process control determine, in production, whether a process shuts down in a controlled way or breaks off mid-operation. SIGTERM gives a process the chance to release open resources, finish requests, and exit on its own. SIGKILL knows none of that leniency and should only ever be a last resort for processes that never respond to SIGTERM. With PHP-FPM the difference is especially tangible: a worker killed by SIGKILL can abort requests mid-processing and leave inconsistent data behind, whereas SIGQUIT triggers a true graceful stop.
trap makes bash scripts signal-safe, pkill and killall make process management by name pattern easier, and systemd along with container runtimes provide TimeoutStopSec, KillSignal, and stop_grace_period as the levers to match stop signals to the application's actual runtime. Combining these tools deliberately replaces kill commands that happen to work by accident with process management that is predictable and safe.
Signals and Process Control: The Key Points at a Glance
SIGTERM vs. SIGKILL
SIGTERM is catchable and polite, SIGKILL acts instantly and without regard. Always send SIGTERM first.
Stopping PHP-FPM cleanly
SIGQUIT triggers a graceful stop, process_control_timeout caps the wait before SIGKILL.
trap in scripts
trap cleanup SIGTERM SIGINT catches signals and releases resources reliably, except for SIGKILL/SIGSTOP.
systemd & containers
Match TimeoutStopSec, KillSignal, and stop_grace_period to the app's real shutdown duration.