Zombie & Defunct Processes
A runbook for diagnosing zombie (defunct) processes on Linux — why they accumulate, why kill -9 doesn't work on them, and how to actually clear them.
Overview
A zombie (shown as Z or <defunct> in ps) is a process that has already finished executing but whose exit status hasn’t been read (wait()-ed on) by its parent yet. It holds essentially no resources except a slot in the process table — but if they accumulate, that table can fill up.
The Problem
ps aux shows one or more processes in state Z, or with <defunct> in their command name, and kill -9 has no effect on them.
Investigation
ps aux | awk '$8 ~ /Z/ {print}' # list all zombie processes
ps -eo pid,ppid,stat,cmd | grep 'Z' # include parent PID — you need this to fix it
# Count zombies system-wide
ps aux | awk '$8=="Z"' | wc -l
# Identify the PARENT process — this is the actual target for a fix
ps -o ppid= -p <zombie_PID>
ps -p <parent_PID> -o cmd
Root Cause Possibilities
- A parent process not calling
wait()— a buggy application (often a custom process-spawning daemon or a shell script forking children) that never reaps its children’s exit status. - A parent process too busy to reap in time — under high load, a brief backlog of unreaped zombies is normal and self-clears; only a persistently growing count is a real problem.
- A parent that has exited without reaping, and init hasn’t been signaled properly — rare on modern systemd systems, more common in custom container entrypoints (see Bash + Docker: the
exec "$@"pattern). - A container’s PID 1 not reaping properly — a shell script as PID 1 in a container, without
tini/dumb-init, doesn’t automatically reap zombies the way a real init system does.
Resolution
# A zombie itself CANNOT be killed — it's already dead. kill -9 on a zombie PID does nothing.
# The only real fixes act on the PARENT:
# Option 1: Signal the parent to reap its children (works if the parent handles SIGCHLD properly)
kill -CHLD <parent_PID>
# Option 2: If the parent is genuinely buggy and won't reap, restart it —
# when the parent dies, its zombie children are re-parented to PID 1 (init/systemd),
# which reaps them immediately.
kill -15 <parent_PID>
# or, if it doesn't respond:
kill -9 <parent_PID>
# In a container: confirm whether PID 1 is a real init (tini/dumb-init) or a bare shell script
docker exec <container> ps -ef # what's actually PID 1 inside?
Prevention
- Always use
tini,dumb-init, or Docker’s--initflag for containers whose entrypoint is a script — a bare shell script as PID 1 doesn’t automatically reap zombies the way real init systems do. - In custom daemons/scripts that fork child processes, always
waitfor them explicitly (see the Processes & Background Jobs tutorial) rather than letting them accumulate. - Monitor zombie process count as a health metric for hosts running custom process-management logic — a slowly climbing count over hours/days indicates a real leak, not transient load.
- End Docker entrypoint scripts with
exec "$@"so the actual application becomes PID 1 directly, inheriting correct signal/reaping behavior from the runtime’s init wrapper.
Summary
A zombie process is dead already — kill -9 on it is a no-op by definition, since it has no running code left to signal. The fix always targets the PARENT: either signal it to reap its children (kill -CHLD), or restart it entirely, which re-parents any remaining zombies to PID 1 for automatic reaping. In containers, the usual root cause is a script running as PID 1 without a real init wrapper.
Pro Tip
A handful of transient zombies appearing and disappearing under load is completely normal — the kernel briefly shows Z state between a child exiting and its parent calling wait(). Only a persistently GROWING zombie count over time is an actual problem worth chasing.
Add More Questions to This Guide
Know a question that should be here? Share it and help the community!
Open Google Form