Why kill -9 does not help, and how to find the parent process
A zombie process has already terminated, yet its parent process never collected its exit status, leaving a blocked entry in the process table. This guide explains the difference between zombies and orphaned processes, shows why kill -9 is completely ineffective, and gives concrete commands to find the responsible parent process and fix the problem permanently in code.
Table of Contents
- 1. What a Zombie Process Really Is
- 2. The Process Lifecycle: fork, exit, and wait()
- 3. Spotting Zombies: ps, the STAT Column, and /proc
- 4. Why Zombies Cannot Be Killed Directly
- 5. Diagnosing the Responsible Parent Process
- 6. Orphaned Processes: The Difference and Re-Parenting
- 7. Typical Causes in Your Own Code
- 8. Fixing Zombie and Orphaned Processes for Good
- 9. Zombie vs. Orphaned Compared
- 10. Summary
- 11. FAQ
1. What a Zombie Process Really Is
A zombie process is not a hung or blocked process, but a process that has already terminated completely. As soon as a process calls exit() or is terminated by a signal, the kernel immediately releases its memory, its file descriptors, and every other resource. No code is running anymore, no CPU time is being consumed. What remains is only a minimal entry in the process table that holds the PID, exit status, and resource usage (rusage) for the parent process. This exact entry is shown by ps as Z (zombie) or <defunct>.
The reason for this intermediate phase lies in the POSIX process model: a parent process should be able to query the exit code of its child, for example to determine whether a sub-task succeeded. This query happens via wait() or waitpid(). Only once the parent process makes this call does the kernel finally remove the table entry, a step commonly called "reaping" the child. If this call never happens, the zombie persists, in theory indefinitely.
2. The Process Lifecycle: fork, exit, and wait()
Understanding zombies requires knowing the full lifecycle of a child process. A process creates a copy of itself with fork(); the child gets its own PID and runs independently, often followed by exec(), which replaces the program image with a new program. When the child terminates, whether normally via exit() or through an uncaught signal, the kernel sends the parent process a SIGCHLD signal and puts the child into state Z.
The parent process can respond in two ways: either synchronously with a blocking wait() call right after starting the child, or asynchronously through a SIGCHLD handler that calls waitpid(-1, &status, WNOHANG) in a loop upon receiving the signal, collecting all pending children at once. If neither happens, the zombie persists until the parent process itself terminates. If the parent process dies, its own zombies are taken over by the init system and reaped there.
3. Spotting Zombies: ps, the STAT Column, and /proc
The STAT column of ps reliably shows zombies with the letter Z, often combined with additional flags such as Zs for a session leader. The command name typically appears as <defunct>, because the original program image has long been removed from memory and the kernel only knows the name from the table entry. For a quick system overview, a filter with awk is useful, extracting all processes with STAT Z and grouping them by parent process.
Alternatively, /proc/<pid>/status provides the same information directly from the kernel: the State field contains exactly Z (zombie) for zombies. This route is especially useful in scripts because it works without an external ps binary and can be parsed directly from /proc. Important for interpretation: a single snapshot says little, since short-lived zombies are perfectly normal as long as the count is not continuously rising over time.
#!/usr/bin/env bash
# List all zombie processes on the system
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/ { print }'
# Count zombies grouped by parent PID, descending
ps -eo ppid,stat | awk '$2 ~ /Z/ { print $1 }' | sort | uniq -c | sort -rn
# Inspect a single process directly via /proc
cat /proc/4821/status | grep -E 'Name|State|PPid'
# Name: sh
# State: Z (zombie)
# PPid: 4711
4. Why Zombies Cannot Be Killed Directly
The instinctive reflex for a suspicious process is kill -9, but this command hits nothing when aimed at a zombie. Signals are delivered to a thread that can process them or terminate because of them. A zombie no longer has a running thread, no address space, no registers, nothing that could receive or react to a signal. The kernel formally delivers the signal, but discards it right away since there is no real recipient. The process remains in state Z, exactly as it was before.
The only way to remove a zombie from the process table is a wait() call by its parent process, or the termination of the parent process itself, which hands responsibility to the init system for cleanup. So anyone wanting to "get rid of" a single zombie must not target the zombie itself, but the parent process. Rebooting the system also reliably removes zombies, but that is not a solution for a recurring problem, only a reset of the symptom.
5. Diagnosing the Responsible Parent Process
The first step is always to determine the zombie's PPID, for example with ps -o ppid= -p <pid>. From there it is worth looking at the parent process itself: is it still running, what program is it, and does it react to SIGCHLD at all? The SigCgt (caught) and SigIgn (ignored) fields in /proc/<ppid>/status show, as a bitmask, whether the process has registered its own handler or explicitly ignores the signal. If neither applies and the process also never calls wait() synchronously, that is the actual root cause.
For deeper analysis, strace -p <ppid> -e trace=wait4,waitid -f shows in real time whether and how often the parent process actually reaps its children. If the output stays empty for several minutes while new zombies keep appearing, the proof is established: the parent process is not reaping its children. For more complex applications, gdb -p <ppid> with a stack trace additionally helps see which function the process is currently stuck in and whether a fork loop without accompanying reaping is present.
#!/usr/bin/env bash
# Find the parent responsible for a known zombie PID
ZOMBIE_PID=4821
PARENT_PID=$(ps -o ppid= -p "$ZOMBIE_PID" | tr -d ' ')
echo "Zombie $ZOMBIE_PID is a child of PID $PARENT_PID"
# Inspect the parent: command line and signal disposition
ps -fp "$PARENT_PID"
grep -E 'SigCgt|SigIgn' /proc/"$PARENT_PID"/status
# Attach strace to see whether the parent ever calls wait()/waitpid()
strace -p "$PARENT_PID" -e trace=wait4,waitid -f -tt
# no output after several minutes => parent never reaps its children
6. Orphaned Processes: The Difference and Re-Parenting
An orphaned process is something fundamentally different from a zombie: it is still running completely normally, only its original parent process has already terminated before the child itself was finished. The kernel solves this problem by immediately assigning the orphan a new parent process, a step known as re-parenting. By default, PID 1 (init or systemd) takes on this role; since Linux 3.4, any process can register itself as a subreaper for its own descendants via prctl(PR_SET_CHILD_SUBREAPER).
An orphaned process is unproblematic on its own, since systemd or init routinely call wait() for all adopted children and reap them cleanly once they terminate. It only becomes critical when the new parent process is itself not a reliable reaper, for example a minimal container entrypoint without any init functionality. In that case a harmless orphan later turns into a permanent zombie, because PID 1 inside the container never assumes the reaping duty. This is known in the Docker world as the "PID 1 problem".
7. Typical Causes in Your Own Code
The most common cause in self-written daemons and worker processes is a fork() without accompanying reaping: a server process spawns a child process for every request but never handles its termination, because it registers no SIGCHLD handler and never calls waitpid() either. Under sustained load, these unreaped children add up to hundreds of zombies until the process table hits its limit and fork() fails with EAGAIN.
A variant of the same problem occurs in container environments: if the application runs as PID 1 inside the container, for example a PHP script that spawns further processes via shell_exec(), nobody takes on the reaper role that the init system handles outside of containers. Shell scripts that start background jobs with & and never wait for them with wait produce the same pattern, usually unnoticed, because the script itself ends shortly after and the zombies fall to the parent process above it.
8. Fixing Zombie and Orphaned Processes for Good
In your own C or C++ code, the most robust solution is a SIGCHLD handler that calls waitpid(-1, &status, WNOHANG) in a loop until no terminated children remain. If you do not need to evaluate the exit status at all, you can explicitly set the disposition of SIGCHLD to SIG_IGN; since POSIX.1-2001, modern Linux kernels then reap children automatically, without them ever entering the zombie state. For classic daemonizing, the double-fork trick prevents the problem structurally: the immediate child process terminates right away, and the grandchild process gets automatically re-parented to the init system, which reaps it reliably.
In container environments, a real init process as PID 1 solves the problem most cleanly. docker run --init, or init: true in Docker Compose, automatically injects tini as PID 1, which takes on exactly this reaping task. Under systemd, you should also avoid manual double-forking and instead use Type=simple or Type=exec, since systemd itself reliably reaps as PID 1 and the double-fork trick becomes unnecessary there.
[Unit]
Description=Custom worker daemon without manual double-fork
After=network.target
[Service]
; Type=simple keeps the process in the foreground - systemd (PID 1)
; becomes the reaper for any children it spawns, no double-fork needed
Type=simple
ExecStart=/usr/local/bin/worker --foreground
Restart=on-failure
KillMode=control-group
[Install]
WantedBy=multi-user.target
services:
worker:
image: mironsoft/php-worker:8.4
# init: true injects tini as PID 1 inside the container,
# which reaps orphaned and zombie grandchild processes
init: true
command: ["php", "bin/worker.php"]
restart: unless-stopped
9. Zombie vs. Orphaned Compared
In practice, the important question is rarely "are there zombies", but "is this a normal, transient state or a real bug". A single zombie that disappears again after a few seconds is part of the normal process lifecycle. A continuously growing count tied to the same parent process, on the other hand, is a clear warning sign of broken reaping in the code.
| Trait | Harmless, Transient Zombie | Problematic, Persistent Zombie | Recommended Action |
|---|---|---|---|
| Lifetime | Under 1 second up to a few seconds | Minutes, hours, or unbounded | Track the value over time, not just once |
| Count on the system | 0 to 2, fluctuating | Steadily increasing count | Capture the trend via monitoring, e.g. node_exporter |
| Parent process behavior | Calls wait()/waitpid() promptly | SIGCHLD ignored, wait() missing entirely | Check with strace -e trace=wait4 against the PPID |
| Typical source | Short-lived shell subprocesses, cron jobs | Your own server or worker process with a fork bug | Code review of the responsible parent process |
| Impact | None, a minimal table entry | PID table exhaustion, fork() fails | Compare pid_max against the current process count |
| Action | None, disappears on its own | Patch the parent process or restart it in a controlled way | Never target the zombie itself, always the parent process |
For lasting confidence, it is worth setting up an automated check that regularly captures the zombie count per parent process and alerts when a threshold is exceeded, instead of discovering zombies only by chance during a manual ps call.
{
"check": "zombie_process_count",
"host": "shop-prod-02",
"timestamp": "2026-07-12T09:15:00Z",
"zombie_count": 3,
"threshold_warning": 5,
"threshold_critical": 20,
"top_parents": [
{ "ppid": 4711, "cmd": "php-fpm: pool magento", "zombies": 3 }
],
"status": "ok"
}
Mironsoft
Linux system administration, monitoring, and DevOps for Magento infrastructure
Ready to professionally fix zombie processes and system instability?
We analyze your Linux servers and Docker containers, identify parent processes with broken reaping, and fix the root cause in code instead of just masking symptoms.
Process Audit
Systematic analysis of every zombie and orphaned process on your production servers
Code Review
Checking fork and signal handling in your own daemons and workers for missing wait() calls
Container Hardening
Securing Docker and Kubernetes setups with correct PID 1 reaping and init processes
10. Summary
A zombie process is a process that has already terminated completely, whose parent process has not yet collected the exit status via wait() or waitpid(). Because no running code exists anymore, kill -9 hits nothing when aimed at a zombie, since there is no recipient for the signal. The fix always lies with the parent process: either fix it directly by adding a SIGCHLD handler, or restart the process in a controlled way so the init system takes over the orphaned children.
Orphaned processes must be clearly distinguished from this: they are still actively running, but were automatically re-parented to PID 1 after the death of their original parent process. They only become critical when this new parent process is itself not a reliable reaper, for example a minimal container entrypoint. The most important practical rule: a single, short-lived zombie is normal, a continuously growing count tied to the same parent process is a bug that must be fixed in the parent process's code, not by repeatedly restarting the server.
Zombie and Orphaned Processes: The Key Points at a Glance
What a Zombie Is
Already terminated via exit(), but the parent process has not called wait() yet. Purely an entry in the process table, no running code.
Why kill Does Not Help
No thread left that could receive a signal. Only the parent process can free the entry through reaping.
Orphaned vs. Zombie
Orphaned processes keep running and are adopted by the init system. Zombies have already terminated and are just waiting to be reaped.
Prevention
A SIGCHLD handler with waitpid(WNOHANG), the double-fork trick when daemonizing, docker run --init or tini in containers.