Guide Linux Intermediate

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.

3 min read

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 --init flag 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 wait for 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