kill -9 on a PID in Z (zombie) state in ps aux does nothing, the process stays zombie. This isn’t a kill bug: a zombie process is already dead, all that remains is an entry in the kernel’s process table, and you cannot kill what’s already dead.

What a zombie actually is

When a process terminates, the kernel keeps its exit status in the process table until the parent process collects it via a wait() or waitpid() system call. Between the process’s actual termination and that collection, the entry still exists but consumes no more memory or CPU: this is the zombie state (Z), a normal and expected transition, not an anomaly by itself.

# Z state = zombie, already terminated,
# waiting for its parent to collect its exit status
ps aux | grep ' Z '
#  user  1234  0.0  0.0      0     0 ?  Z  10:03  0:00 [my-worker] <defunct>

Why the problem is never the child

An isolated zombie, in transit while the parent processes the SIGCHLD event, resolves itself within moments. The real problem shows up when a parent never collects its terminated children, usually an application bug (a fork loop missing the corresponding wait(), or error handling that skips the collection call): zombies accumulate indefinitely, each one occupying a process table entry until the machine potentially runs out of available PIDs.

# Counts zombies: a continuous accumulation points
# to a collection bug in the parent, not the child
ps -eo stat | grep -c '^Z'

The actual fix: repair or restart the parent

Since a zombie can’t be killed, the fix always targets the parent: either fix its code to call wait()/waitpid() on its terminated children, or simply restart it. Restarting the parent automatically frees every zombie it never collected: when the parent dies, its zombie children get reassigned (“reparented”) to init (PID 1, or systemd on most modern distributions), which collects them automatically and immediately. This reparenting mechanism is the same one that guarantees a systemd-managed service never leaves orphaned zombies lingering indefinitely on the system.

Takeaway

A zombie process is already dead, SIGKILL can do nothing more against it: only a process table entry remains, waiting to be collected by its parent via wait(). A continuous accumulation of zombies systematically points to a collection bug in the parent’s code, never in the terminated child. Restarting (or fixing) the parent is the only real fix, with automatic reparenting to PID 1 then cleanly collecting whatever was left pending.