If ps keeps showing lines ending in <defunct>, your program is leaking zombie processes. It is one of the most common bugs in C daemons, servers and any code that calls fork(). This guide explains exactly why zombies appear, how to create one on purpose so you can study it, and three reliable ways to prevent zombie processes in C: waitpid() with WNOHANG, a SIGCHLD handler, and the double fork technique.
Every section includes the real ps output you should see before and after the fix, so you can verify the result on your own machine instead of trusting the code blindly.
What Is a Zombie Process, Really?
A zombie (state Z, shown by ps as <defunct>) is a child process that has already terminated but whose exit status has not yet been collected by its parent.
When a child calls exit() or is killed, the kernel releases almost everything: memory, file descriptors, sockets, locks. What it keeps is a tiny record in the process table containing the PID, the exit code, and some accounting data (CPU time, signal that killed it). That record exists for one reason only: the parent may still want to ask “how did my child end?” through wait() or waitpid().
As soon as the parent reaps the child, the entry disappears. If the parent never reaps it, the entry stays forever and you get a zombie. Anyone digging further should read Creating and Killing Child Processes in C.
Why Zombies Are a Problem
- They consume no CPU and almost no memory, so a single zombie is harmless.
- They consume a PID slot. A loop that forks thousands of unreaped children will exhaust
kernel.pid_max(or yourRLIMIT_NPROC), and thenfork()starts returningEAGAINfor the whole user or the whole system. - They are a symptom: a program that ignores child exit statuses usually also ignores child failures.
- In containers, an unreaped process table is one of the classic causes of a container that slowly stops being able to spawn anything.
Zombie vs Orphan: Not the Same Thing
| Term | State of the child | State of the parent | Who cleans up? |
|---|---|---|---|
| Zombie | Dead (Z / defunct) | Alive but not calling wait() | Nobody, until the parent dies |
| Orphan | Still running | Already dead | Reparented to PID 1 (systemd), which reaps it later |
Key consequence: an orphan is never a leak, because init reaps it. A zombie is a leak only while its parent stays alive. This single fact is what makes the double fork technique work, as you will see below.

Step 1: Reproduce a Zombie Process in C
Save this as zombie.c. The child exits immediately, the parent sleeps and never calls wait().
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
/* child */
printf("child %d exiting now\n", (int)getpid());
_exit(0);
}
/* parent: never reaps the child */
printf("parent %d sleeping, child is %d\n", (int)getpid(), (int)pid);
sleep(120);
return 0;
}
Build and run it in the background:
$ gcc -Wall -Wextra -o zombie zombie.c
$ ./zombie &
[1] 4821
parent 4821 sleeping, child is 4822
child 4822 exiting now
$ ps -o pid,ppid,stat,comm -p 4821,4822
PID PPID STAT COMMAND
4821 4818 S zombie
4822 4821 Z zombie <defunct>
PID 4822 is your zombie. Notice the STAT column showing Z.
Useful Commands to Find Zombies
# list every zombie with its parent PID
$ ps -eo pid,ppid,stat,comm | awk '$3 ~ /^Z/'
# just count them
$ ps -eo stat | grep -c '^Z'
# who is the guilty parent?
$ ps -o pid,ppid,cmd -p $(ps -eo ppid,stat | awk '$2 ~ /^Z/ {print $1}' | sort -u | tr '\n' ',' | sed 's/,$//')
You can also try kill -9 <zombie_pid>. Nothing happens: the process is already dead, signals cannot kill it. The only way to remove a zombie is to make its parent reap it, or to kill the parent so the zombie is reparented to PID 1 and reaped there. That is a workaround for production, not a fix. The fix belongs in your C code.
Fix 1: waitpid() with WNOHANG (Non-Blocking Reaping)
The simplest correct fix is to call wait() or waitpid(). The problem is that plain wait() blocks, which is unacceptable in a server that must keep accepting connections.
WNOHANG solves this: waitpid() returns immediately, with 0 if children exist but none has exited yet.
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
/* Call this regularly from your main loop. */
void reap_children(void)
{
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status))
printf("child %d exited with code %d\n",
(int)pid, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("child %d killed by signal %d\n",
(int)pid, WTERMSIG(status));
}
if (pid < 0 && errno != ECHILD)
perror("waitpid");
}
Three Details People Get Wrong
- Use a
whileloop, not a single call. Several children can exit between two iterations of your main loop. -1means “any child”. Use a specific PID only when you truly need that one child’s status.ECHILDis not an error in this context, it just means there is nothing left to reap.
Add reap_children() at the top of your accept loop, or right after poll() / epoll_wait() returns. Verification:
$ ps -eo pid,ppid,stat,comm | awk '$3 ~ /^Z/'
$ # empty output = zero zombies
Limitation: if your main loop blocks for a long time in epoll_wait(), zombies survive until the next wake-up. That is where the signal handler comes in. geeksforgeeks.org has covered this at length.

Fix 2: A SIGCHLD Handler (The Event-Driven Way)
The kernel sends SIGCHLD to the parent every time a child changes state. Instead of polling, react to that signal.
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
static void sigchld_handler(int sig)
{
int saved_errno = errno; /* waitpid() can clobber errno */
(void)sig;
while (waitpid(-1, NULL, WNOHANG) > 0)
;
errno = saved_errno;
}
int install_sigchld_handler(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
return sigaction(SIGCHLD, &sa, NULL);
}
Rules for a safe SIGCHLD handler:
- Use
sigaction(), neversignal().signal()semantics differ between systems and may reset the handler. - Loop with
WNOHANG. Standard signals are not queued: three children dying at once may produce a singleSIGCHLD. Without the loop you leak two zombies. - Save and restore
errno, otherwise you can corrupt error handling in the interrupted code. SA_NOCLDSTOPavoids being woken when a child is merely stopped (SIGSTOP) rather than terminated.SA_RESTARTrestarts most interrupted syscalls, but not all of them. Keep handlingEINTRaroundaccept(),read(),poll()anyway.- Only async-signal-safe functions inside the handler. No
printf(), nomalloc(). If you need to log, set avolatile sig_atomic_tflag and do the work in the main loop.
Variant: SIG_IGN and SA_NOCLDWAIT
If you genuinely do not care about exit statuses, POSIX lets you tell the kernel to auto-reap:
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = SIG_IGN; /* or a real handler + SA_NOCLDWAIT */
sa.sa_flags = SA_NOCLDWAIT;
sigaction(SIGCHLD, &sa, NULL);
Children are then reaped automatically and no zombie is ever created. Two warnings:
- You permanently lose the ability to get exit codes, and
wait()will fail withECHILD. - The
SIG_IGNdisposition is inherited acrossexec(). A library or a child program that relies onsystem()orpopen()may then break. Reset it toSIG_DFLin the child betweenfork()andexec().
Modern Alternative: signalfd or pidfd
If you already run an epoll loop, avoid handler-context constraints entirely:
signalfd(SIGCHLD): turn the signal into a readable file descriptor, then callwaitpid(WNOHANG)in your normal loop.pidfd_open()(Linux 5.3 and later, so available on every currently supported distribution): get a file descriptor for one specific child, poll it, and it becomes readable when the child dies. No PID reuse races. You still callwaitpid()to collect the status.
Fix 3: The Double Fork Technique
Sometimes you cannot wait at all: you want to launch a long-running background job and completely forget about it. This is where the double fork (fork twice) pattern belongs.
The idea: make the real worker a grandchild instead of a child. The intermediate child exits instantly, the parent reaps it instantly, and the grandchild becomes an orphan adopted by PID 1, which reaps it automatically when it finishes.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
/* Launch a command with no zombie and no need to ever wait for it. */
int spawn_detached(char *const argv[])
{
pid_t pid = fork();
if (pid < 0)
return -1;
if (pid == 0) {
/* intermediate child */
pid_t pid2 = fork();
if (pid2 < 0)
_exit(127);
if (pid2 == 0) {
/* grandchild: the real work */
setsid(); /* optional: detach from the terminal */
execvp(argv[0], argv);
_exit(127); /* exec failed */
}
_exit(0); /* intermediate child exits immediately */
}
/* parent: this wait returns almost instantly */
return waitpid(pid, NULL, 0) == pid ? 0 : -1;
}
Verify the reparenting. Start a detached sleep 300 and look at the PPID column:
$ ps -eo pid,ppid,stat,cmd | grep '[s]leep 300'
5177 1 S sleep 300
$ ps -eo stat | grep -c '^Z'
0
PPID is 1: the grandchild now belongs to systemd, and no zombie can accumulate in your program regardless of how long the job runs.
When NOT to Use Double Fork
- You need the exit status of the job. You lose it (use a pipe, a status file or a proper job queue instead).
- You want the job to die with your program. An orphan survives its original parent, so add explicit lifecycle management or use
prctl(PR_SET_PDEATHSIG)in the grandchild. - You are inside a container whose PID 1 is your own application and does not reap. See the container section below.

Comparing the Three Strategies
| Strategy | Exit status available? | Blocking? | Best for | Main pitfall |
|---|---|---|---|---|
| waitpid + WNOHANG in main loop | Yes | No | Loops that iterate often (accept, poll, epoll) | Zombies linger if the loop blocks for a long time |
| SIGCHLD handler | Yes | No | Daemons, forking servers, general purpose | Missing the while loop; EINTR; unsafe calls in the handler |
| SIG_IGN / SA_NOCLDWAIT | No | No | Fire and forget helpers | Inherited across exec, breaks system()/popen() in children |
| Double fork | No | Very briefly | Detached background jobs, daemonizing | Job outlives the parent; no status reporting |
Special Case: Your Program Runs as PID 1 (Containers)
Inside a container, your application often is PID 1. The double fork trick then reparents grandchildren to… your own process. If your code does not reap, they pile up as zombies and nothing else will clean them.
Two options:
- Implement a real
SIGCHLDreaping loop (Fix 2) in whatever runs as PID 1. - Use a minimal init:
docker run --init,tini,dumb-init, or a systemd unit outside the container. They exist precisely to forward signals and reap orphans.
Quick check inside a container:
$ ps -eo pid,ppid,stat,comm
PID PPID STAT COMMAND
1 0 Ss my-daemon
42 1 Z worker <defunct> <-- PID 1 is not reaping

Troubleshooting Checklist
- Confirm the zombies exist:
ps -eo pid,ppid,stat,comm | awk '$3 ~ /^Z/' - Identify the parent from the
PPIDcolumn, thenps -p <ppid> -o pid,cmd. - Read that parent’s source and locate every
fork(). Each one must have a matching reaping path. - Check the handler loops, not just a single
waitpid()call. - Check for a swallowed EINTR: a
waitpid()interrupted by a signal and treated as a fatal error will silently stop reaping. - Temporary relief in production:
kill -CHLD <ppid>can trigger reaping if the parent has a working handler. Otherwise restart the parent, which hands the zombies to PID 1. - Regression test: fork 1000 children in a loop, then assert
ps -eo stat | grep -c '^Z'returns 0.
Complete Working Example: A Forking Server With Zero Zombies
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
static void sigchld_handler(int sig)
{
int saved_errno = errno;
(void)sig;
while (waitpid(-1, NULL, WNOHANG) > 0)
;
errno = saved_errno;
}
int main(void)
{
struct sigaction sa;
int i;
memset(&sa, 0, sizeof sa);
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
if (sigaction(SIGCHLD, &sa, NULL) < 0) {
perror("sigaction");
return 1;
}
for (i = 0; i < 1000; i++) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
break;
}
if (pid == 0) {
/* pretend to serve a request */
usleep(1000);
_exit(0);
}
}
sleep(3); /* let every child finish and be reaped */
printf("parent %d done, check for zombies now\n", (int)getpid());
sleep(30);
return 0;
}
$ gcc -Wall -o server server.c && ./server &
$ ps -eo stat | grep -c '^Z'
0
Remove the sigaction() call and rerun: the counter jumps to 1000. That contrast is the whole lesson.
FAQ
What causes a zombie process?
A child process terminates and the parent never calls wait() or waitpid() to collect its exit status. The kernel keeps a small process table entry until the status is read. This is the sort of thing a solid web development studio ships without fuss.
How do I get rid of zombie processes in Linux?
You cannot kill a zombie directly, it is already dead. Make the parent reap it (send SIGCHLD if it has a handler), or terminate the parent so the zombie is reparented to PID 1 and cleaned up. The permanent fix is reaping in the parent’s code.
How do I know if a process is a zombie?
Run ps -eo pid,ppid,stat,comm and look for state Z or the text <defunct>. top also shows a zombie counter in its summary line.
Does WNOHANG alone prevent zombie processes?
Only if you actually call it, repeatedly, in a loop that runs often enough. WNOHANG makes reaping non-blocking, it does not schedule it for you.
Is one waitpid() call per SIGCHLD enough?
No. Standard signals are not queued, so several simultaneous child deaths can be merged into a single SIGCHLD. Always loop with while (waitpid(-1, NULL, WNOHANG) > 0);.
Do zombie processes use memory or CPU?
Essentially none. The real cost is a PID slot and a process table entry, which matters only when they accumulate by the thousands.
Should I use double fork or a SIGCHLD handler?
Use a SIGCHLD handler when you care about exit statuses and manage the children’s lifecycle. Use the double fork when you want a fully detached background job and never intend to wait for it.
Why do I still see zombies even though I use double fork?
Most likely your process is PID 1 (typical in containers) or you are inside a PID namespace where your program is the init. In that case the reparented grandchildren land on you, and you must reap them yourself or run a minimal init such as tini.
Key Takeaways
- Every
fork()needs a matching reap. No exceptions. - Default choice: a
sigaction()-installedSIGCHLDhandler containing awaitpid(-1, ..., WNOHANG)loop, witherrnosaved and restored. - Fire and forget jobs: double fork, and let PID 1 do the cleanup.
- Do not care about statuses at all:
SA_NOCLDWAIT, but reset the disposition beforeexec(). - Always verify with
ps -eo stat | grep -c '^Z'under load, not just once at startup.