Version
@playwright/mcp@0.0.75 (which bundles playwright-core@1.60.0). The relevant code is in playwright-core, not in the @playwright/mcp wrapper, which is why I'm filing here.
Steps to reproduce
Save as /tmp/host.mjs:
import { spawn } from 'node:child_process';
const child = spawn('npx', ['-y', '@playwright/mcp@latest'], {
stdio: ['pipe', 'pipe', 'inherit'],
});
const send = (msg) => child.stdin.write(JSON.stringify(msg) + '\n');
let id = 0;
send({ jsonrpc: '2.0', id: ++id, method: 'initialize',
params: { protocolVersion: '2024-11-05', capabilities: {},
clientInfo: { name: 'repro', version: '1' } } });
setTimeout(() => send({ jsonrpc: '2.0',
method: 'notifications/initialized' }), 500);
setTimeout(() => send({ jsonrpc: '2.0', id: ++id, method: 'tools/call',
params: { name: 'browser_navigate',
arguments: { url: 'about:blank' } } }), 1500);
console.error('host pid:', process.pid, 'child pid:', child.pid);
Then in a shell:
node /tmp/host.mjs &
HOST_PID=$!
sleep 10 # let chrome boot
echo "=== before kill ==="
ps -eo pid,ppid,etime,cmd | grep -E 'mcp-chrome|chrome.*--user-data-dir' | grep -v grep
kill -9 $HOST_PID # simulates IDE hard-reload / crash
sleep 3
echo "=== after host SIGKILL ==="
ps -eo pid,ppid,etime,cmd | grep -E 'mcp-chrome|chrome.*--user-data-dir' | grep -v grep
Real-world equivalents that hit the same path: the user reloads their IDE
window (VS Code "Developer: Reload Window"), the OS OOM-kills the IDE, or
the host process exits abnormally for any reason that doesn't propagate
SIGINT/SIGTERM to the MCP server child.
Expected behavior
After kill -9 $HOST_PID, the Chrome process tree spawned by
@playwright/mcp should exit. No chrome processes should remain re-parented
to PID 1, and the per-session mcp-chrome-* directory under
~/.cache/ms-playwright/ should be eligible for cleanup.
Actual behavior
After kill -9 $HOST_PID, the Chrome process tree is still running and has
been re-parented to PID 1. Sample from the repro on my machine:
=== after host SIGKILL ===
pid ppid etime cmd
12847 1 00:14 /home/alexkahn/.cache/ms-playwright/chromium_headless_shell-1207/chrome-linux/headless_shell --user-data-dir=/home/alexkahn/.cache/ms-playwright/mcp-chrome-... --remote-debugging-pipe ...
12851 1 00:14 [chrome zygote]
12858 12847 00:14 [chrome gpu-process]
...
The chrome tree stays alive indefinitely; etime keeps growing. Over a
typical multi-day developer session this accumulates to multiple orphan
trees, each holding 200–500 MB resident. On WSL with a fixed memory cap
this drives the VM toward swap. A pgrep -f 'ms-playwright/mcp-chrome' | xargs kill -9 reliably frees hundreds of MB at a time on real machines
that have only been doing normal browser-automation work.
Additional context
The watchdog
(packages/playwright-core/src/server/launchApp.ts and the bundled MCP watchdog
in coreBundle) currently registers handlers for:
process.on('SIGINT', …)
process.on('SIGTERM', …)
process.stdin.on('close', …)
It also schedules a 15-second hard exit:
setTimeout(() => process.exit(0), 15_000);
The exit paths this misses:
- Parent SIGKILL — no signal reaches the MCP server child, so neither
SIGINT nor SIGTERM fires. The classic detection is PPID polling (the
process is re-parented to PID 1, i.e. process.ppid === 1).
npx / npm exec intermediary — the standard MCP host config is
{ "command": "npx", "args": ["@playwright/mcp@latest"] }. The npx
wrapper sometimes swallows the stdin-close propagation, so even a
graceful host shutdown doesn't trigger stdin('close') in the
grandchild.
- Hard-exit timeout doesn't kill chrome. The 15s
setTimeout calls
process.exit(0) without invoking the kill set first. So even when the
watchdog does run, a slow gracefullyCloseAll() (which is what holds
it open past 15s in the first place) results in chrome being abandoned.
There are two prior reports of essentially the same bug, both closed as
"no repro":
I think the "no repro" closures happened because the bug doesn't appear on
Playwright's own CI matrix (clean spawn, clean SIGTERM on teardown). It
only appears with a real-world MCP host setup where (a) the host can die
without sending SIGTERM and (b) there's typically an npx/npm exec
intermediary in the spawn chain. The script above produces it
deterministically.
Environment
System:
OS: Linux 6.18 Ubuntu 24.04.4 LTS 24.04.4 LTS (Noble Numbat)
CPU: (10) x64 Intel(R) Core(TM) Ultra 7 155U
Memory: 5.34 GB / 11.68 GB
Container: Yes
Binaries:
Node: 22.14.0 - /home/alexkahn/.nvm/versions/node/v22.14.0/bin/node
Yarn: 1.22.22 - /home/alexkahn/.nvm/versions/node/v22.14.0/bin/yarn
npm: 10.9.2 - /home/alexkahn/.nvm/versions/node/v22.14.0/bin/npm
pnpm: 10.32.1 - /home/alexkahn/.bun/bin/pnpm
bun: 1.3.14 - /home/alexkahn/.bun/bin/bun
npmPackages:
@playwright/mcp: 0.0.75
playwright-core: 1.60.0
(WSL2 on Windows 11; the bug also reproduces on plain Linux per the prior
reports linked above.)
Version
@playwright/mcp@0.0.75(which bundlesplaywright-core@1.60.0). The relevant code is inplaywright-core, not in the@playwright/mcpwrapper, which is why I'm filing here.Steps to reproduce
Save as
/tmp/host.mjs:Then in a shell:
Real-world equivalents that hit the same path: the user reloads their IDE
window (VS Code "Developer: Reload Window"), the OS OOM-kills the IDE, or
the host process exits abnormally for any reason that doesn't propagate
SIGINT/SIGTERM to the MCP server child.
Expected behavior
After
kill -9 $HOST_PID, the Chrome process tree spawned by@playwright/mcpshould exit. No chrome processes should remain re-parentedto PID 1, and the per-session
mcp-chrome-*directory under~/.cache/ms-playwright/should be eligible for cleanup.Actual behavior
After
kill -9 $HOST_PID, the Chrome process tree is still running and hasbeen re-parented to PID 1. Sample from the repro on my machine:
The chrome tree stays alive indefinitely;
etimekeeps growing. Over atypical multi-day developer session this accumulates to multiple orphan
trees, each holding 200–500 MB resident. On WSL with a fixed memory cap
this drives the VM toward swap. A
pgrep -f 'ms-playwright/mcp-chrome' | xargs kill -9reliably frees hundreds of MB at a time on real machinesthat have only been doing normal browser-automation work.
Additional context
The watchdog
(
packages/playwright-core/src/server/launchApp.tsand the bundled MCP watchdogin
coreBundle) currently registers handlers for:process.on('SIGINT', …)process.on('SIGTERM', …)process.stdin.on('close', …)It also schedules a 15-second hard exit:
The exit paths this misses:
SIGINT nor SIGTERM fires. The classic detection is PPID polling (the
process is re-parented to PID 1, i.e.
process.ppid === 1).npx/npm execintermediary — the standard MCP host config is{ "command": "npx", "args": ["@playwright/mcp@latest"] }. Thenpxwrapper sometimes swallows the stdin-close propagation, so even a
graceful host shutdown doesn't trigger
stdin('close')in thegrandchild.
setTimeoutcallsprocess.exit(0)without invoking the kill set first. So even when thewatchdog does run, a slow
gracefullyCloseAll()(which is what holdsit open past 15s in the first place) results in chrome being abandoned.
There are two prior reports of essentially the same bug, both closed as
"no repro":
root-cause analysis.
via
npm exec.I think the "no repro" closures happened because the bug doesn't appear on
Playwright's own CI matrix (clean spawn, clean SIGTERM on teardown). It
only appears with a real-world MCP host setup where (a) the host can die
without sending SIGTERM and (b) there's typically an
npx/npm execintermediary in the spawn chain. The script above produces it
deterministically.
Environment
(WSL2 on Windows 11; the bug also reproduces on plain Linux per the prior
reports linked above.)