Repository navigation
[Browser plugin] playwright run-server is never killed on Debian/Ubuntu (/bin/sh = dash): orphaned node process after every run, terminal hangs when output is piped #1754
Description
Activity
Confirming this is still present in v5 — the code path is unchanged from the 4.x analysis above.
Environment
pestphp/pestv5.0.3,pestphp/pest-plugin-browserv5.0.0- PHP 8.5.8, Laravel Sail runtime on Ubuntu 24.04,
/bin/sh→ dash - playwright 1.61.1, Chromium headless
ServerManager::playwright()still builds the string command withoutexec, andPlaywrightNpmServer::start()still runs it throughProcess::fromShellCommandline(), so the diagnosis carries over verbatim. After a clean, fully passing run:$ ./vendor/bin/pest tests/Browser/HelpMenuTest.php Tests: 1 passed (7 assertions) $ ps -eo pid,ppid,etimes,rss,args | grep '[p]laywright run-server' 72948 1 3 176740 node ./node_modules/.bin/playwright run-server --host 127.0.0.1 --port 37527 --mode launchServerPPID 1, ~176 MB RSS, one per run. No interruption or failure needed — a green run leaks too.
A second symptom worth adding: it surfaces as flaky browser tests, not just a hanging terminal.
We chased this for a while as a flaky test, because the accumulation degrades the machine before it breaks it. Eight consecutive runs of the same 2-test file, measuring after each:
run orphans container free RAM 1 1 5753 MB 2 2 5556 MB 4 4 5247 MB 6 6 4969 MB 8 8 4669 MB Linear, ~150 MB per run. Over two days of ordinary development we had 65 orphaned servers and 121 MB free of 7.9 GB. At that point browser interactions start stalling past the plugin's 5 s timeout, and each suite run fails a different test —
HelpMenuTesthere,ResponsiveLayoutTestthere — which reads exactly like flakiness in the tests themselves. Raising the timeout 5 s → 15 s did not help (the failures simply became 15 s timeouts, and runs went from ~20 s to 33–76 s); killing the orphans did, and five consecutive suite runs went green with memory flat.So for anyone landing here from a flaky-browser-test search: this is likely your cause, and
pkill -f '[p]laywright run-server'before each run is a workable stopgap until theexecfix lands.Happy to test a patch on this environment if that helps.
Status update, and a pointer to the PR that fixes this.
Thanks @leonardoaugustus for the v5 confirmation — that matches what is in the tree today. On
5.x,ServerManagerstill builds the command as./node_modules/.bin/playwright run-server --host %s --port %d --mode launchServerwith noexecprefix (the line is byte-identical to4.x), andPlaywrightNpmServer::start()still runs it throughProcess::fromShellCommandline(). One thing did change — #172 ("Stop playwright with default signals") dropped the explicitSIGTERMfromstop()— but that does not help here: the signal still reaches theshwrapper, notnode.pestphp/pest-plugin-browser#254 fixes exactly this, against
5.x: it adds theexecprefix sonodereplaces the shell andstop()signals the real process, with a unit test on the composed command line. It supersedes #169 and #211 (both aimed at the dormant4.x). That PR is the right place to review this.Leaving this open until it is merged, since the orphaned
nodeprocess is still reproducible on a released version.
Update, 29 September 2026 — confirmed on a third environment, and #254 verified as the fix.
WSL2 (Ubuntu,
/bin/sh→ dash), Linux 6.18 kernel on a Windows host, PHP 8.4.24, Playwright 1.62.1, plugin5.xatc98e8a5. Same test file in both arms, identical outcome per run (1 failed, 26 skipped, 2 passed; the failure is an unrelated external-URL test), counting the servers left behind:run stock 5.xwith pestphp/pest-plugin-browser#254 1 1 orphan 0 2 2 orphans 0 3 3 orphans 0 ~182 MB RSS each. On WSL2 the orphans are reparented to
/initrather than to PID 1 by name, which is the same thing under a different label — so this environment reads differently inpswhile being the same defect.The accumulation is as silent here as @leonardoaugustus described: before I went looking, five ordinary runs earlier in the day had left five servers and ~730 MB behind, with every run green and nothing in the output to suggest it.
One correction to my previous comment: #254 has grown since. As of 28 September it is no longer only the
execprefix — it also keeps body-less responses off keep-alive connections. I verified only the process half, which is what this issue is about.thank you guys, i hope this will be available soon, it causes a lot of issues when ai agents works for you 😄
Another confirmation, on an architecture not covered here yet, plus a data point about parallel runs that I did not see mentioned above.
Environment
pestphp/pestv5.0.2,pestphp/pest-plugin-browserv5.0.0- PHP 8.4.25, Ubuntu 24.04 aarch64 (Oracle ARM server, Docker),
/bin/sh→ dash - Playwright 1.62.0, Chromium headless
Parallel runs leak once per run, not once per worker.
Full suite,
--parallel --processes=8, 620 tests of which 4 are browser tests, all green:$ pgrep -cf '[p]laywright run-server' # before 0 $ vendor/bin/pest --parallel --processes=8 Tests: 618 passed, 2 skipped (620 total) $ ps -eo pid,ppid,rss,args | grep '[p]laywright run-server' 3808309 1 128860 node ./node_modules/.bin/playwright run-server --host 127.0.0.1 --port 55169 --mode launchServerThat lines up with the code:
ServerManager::playwright()only builds aPlaywrightNpmServerwhenParallel::isWorker()is false, so the one server started in the main process during collection (UsesBrowserTestCaseMethodFilter::accept()) is the only one, and the workers attach to it viaAlreadyStartedPlaywrightServer::fromPersisted(). So--parallelneither multiplies nor avoids the leak:Plugin::terminate()does reachplaywright()->stop()in the main process, and it still does not kill anything, because the signal lands on the dash wrapper exactly as diagnosed above.The orphan also outlives the parent being killed outright. Running the browser file under
timeout 600 php vendor/bin/pest tests/Browser/ConsoleFlowTest.php, the PHP process is gone when the limit fires and the server is left at PPID 1 with the same RSS. Same root cause seen from the other side; overlaps #1825.Accumulation here: 14 servers / ~1.4 GB RSS over 13 days of ordinary development, every run green and nothing in the output hinting at it. We only went looking because of the memory.
Confirming the released tag is still affected:
PlaywrightNpmServer::start()in v5.0.0 has noexecin the composed command line (grep -n execon that file returns nothing), so pestphp/pest-plugin-browser#254 is still the fix for the version people are installing today.Stopgap in the meantime, for anyone else landing here:
pkill -f '[p]laywright run-server'as the first step of the composertestscripts.
Description
After every pest run that includes browser tests, the
playwright run-servernode processsurvives the pest process on Debian/Ubuntu-based environments (including Laravel Sail
runtimes). One orphan is leaked per run.
The most visible symptom: when pest output is piped (
pest ... 2>&1 | cat, CI logcollectors, etc.), the terminal hangs forever after the final test summary — the orphaned
server inherits the stdout pipe, so the reader never gets EOF. Without a pipe the prompt
returns, but orphaned node processes silently accumulate until the machine/container is
restarted.
Environment
4.xHEAD)/bin/sh→ dash)Root cause
ServerManager::playwright()builds the server command as a string andPlaywrightNpmServer::start()runs it viaProcess::fromShellCommandline(). A stringcommand goes through
proc_open→/bin/sh -c '...', and Symfony Process only prependsexecfor array commandlines — not forfromShellCommandline().On Debian/Ubuntu
/bin/shis dash, which does not exec-replace itself with a single-ccommand. The resulting process tree is:PlaywrightNpmServer::stop()calls$process->stop(timeout: 0.1, signal: SIGTERM), whichsignals only the direct child (
sh).shdies, the node server is reparented to PID 1and keeps running forever. (The node process itself handles SIGTERM fine — killing it
directly works instantly.)
On macOS
/bin/shis bash, which does exec-replace a single-ccommand, so the signalreaches node directly and everything shuts down cleanly — the bug is invisible there.
Standalone reproduction (no pest involved)
Observed output on Ubuntu (
/bin/sh→ dash):Suggested fix (verified)
Prepend
execto the shell command inServerManager::playwright()(non-Windows):With
exec,shreplaces itself with node,getPid()points at the server itself, andstop()kills it reliably. Verified with the repro above — the tree collapses tophp → nodeand no survivors remain afterstop(). Also verified end-to-end with a realbrowser test run: the terminal returns immediately even with piped output, and
pgrep -af 'playwright run-server'is clean afterwards.Alternatively, use the array form of
Process(no shell at all). Two smaller relatedobservations spotted while debugging this:
PlaywrightNpmServer::stop()passesSIGTERMas the fallback signal ofProcess::stop(), so it never escalates to SIGKILL; passingnull(default SIGKILL)would be more robust.
Plugin::terminate()swallows the RevoltError(«Must call resume() or throw() beforecalling suspend() again») with an early
returnbefore stopping the playwright server —if that path is ever taken, the server would leak even with the signal fix.