Skip to content

Reap orphaned Chrome processes to stop exhausting the PID limit - #13

Merged
moufmouf merged 1 commit into
mainfrom
fix/zombie-process-leak
Jul 20, 2026
Merged

Reap orphaned Chrome processes to stop exhausting the PID limit#13
moufmouf merged 1 commit into
mainfrom
fix/zombie-process-leak

Conversation

@moufmouf

Copy link
Copy Markdown
Contributor

The problem

The staging monitoring pod was reporting test failures that had nothing to do with the test. Playwright was surfacing:

Error: browserType.launch: Target page, context or browser has been closed
[pid=650176][err] FATAL:zygote_host_impl_linux.cc(184)] Check failed: process.IsValid().
                  Failed to launch zygote process

That's Chromium failing to fork(). The pod had been up 19 days:

pids.current  18560
pids.max      18597     <- 37 PIDs left
ps -eo stat   18515 Z   <- all parented to PID 1

Chromium leaves helper processes behind on every run. They get re-parented to PID 1, which was npm run start — npm is not an init and never calls wait(), so the orphans stayed as zombies forever, each holding a PID slot.

Note that the pod stayed Running 1/1 the entire time. It degraded silently over weeks.

The fix

Dockerfile — install tini and make it the entrypoint, so PID 1 actually reaps orphans.

src/index.ts — the timeout path was orphaning browsers at the source. exec("npm run test") spawns sh -c ..., so childProcess.kill() killed only that shell and cut npm → playwright → chrome loose. Switched to spawn with detached: true so each run gets its own process group, and the kill now takes the whole tree via process.kill(-pid, 'SIGKILL').

Moving from exec to spawn also means streaming stdout/stderr as they arrive rather than buffering the whole run.

Verification

npm run typecheck passes. Staging was restarted to clear the backlog (zombies can't be reaped without an init — only a restart clears them), and after one run on the current image:

pids.current  47      (was 18560)
2 Z                   <- 2 zombies per run, the leak in miniature
1 passed (6.7s)

2 zombies/run × 288 runs/day at the default 300s interval is how it reached 18,515 in ~19 days. The restart buys a few weeks; this image change is the actual fix.

Not addressed here

helm/values.yaml has resources: {}, so the pod is unlimited. A memory limit would have turned this into a loud OOMKill weeks earlier instead of a silently degrading pod. Worth a follow-up.

🤖 Generated with Claude Code

Chromium leaves helper processes behind on every Playwright run. They get
re-parented to PID 1, which was `npm run start` -- npm never calls wait(),
so they accumulated as zombies until the container hit its cgroup PID limit
and fork() started failing.

The symptom was Chromium reporting "Failed to launch zygote process", which
Playwright surfaces as "Target page, context or browser has been closed" --
it reads as a test failure, but the tests were fine. A staging pod up for 19
days had 18515 zombies and 37 PIDs left.

Two changes:

- Add tini as the entrypoint so PID 1 reaps orphans.
- The timeout path was orphaning browsers too: exec() spawns `sh -c ...` and
  childProcess.kill() killed only that shell, cutting npm -> playwright ->
  chrome loose. Use spawn() with detached: true so the run gets its own
  process group, and kill the whole tree with process.kill(-pid).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@moufmouf
moufmouf force-pushed the fix/zombie-process-leak branch from 086fabb to 3a139ee Compare July 20, 2026 11:57
@moufmouf
moufmouf merged commit e358a80 into main Jul 20, 2026
1 check passed
@moufmouf
moufmouf deleted the fix/zombie-process-leak branch July 20, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant