Repository navigation
[Bug]: UI mode fails with spawn EBADF on macOS when the test directory contains more than ~10k files #43653
Description
Version
1.64.1 (also reproduced on 1.63.0)
Steps to reproduce
-
Create a project with this
playwright.config.ts:import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './', testMatch: '**/*.spec.ts', });
-
Add one trivial test,
example.spec.ts:import { expect, test } from '@playwright/test'; test('passes', () => { expect(1 + 1).toBe(2); });
-
Put 11,000 plain files under
testDir:mkdir src && for i in $(seq 0 10999); do echo "export const v$i = $i;" > src/file-$i.ts; done
-
Run
npx playwright test --uiand click Run all.
Expected behavior
The test runs and passes, as it does with npx playwright test without --ui.
Actual behavior
No test runs (0/0). The output pane shows:
Error: spawn EBADF
Error: spawn EBADF
The full stack, from a real project on 1.63.0:
Error: spawn EBADF
at ChildProcess.spawn (node:internal/child_process:486:11)
at spawn (node:child_process:860:9)
at Object.fork (node:child_process:177:10)
at WorkerHost.startRunner (node_modules/playwright/lib/runner/index.js:1915:49)
at WorkerHost.start (node_modules/playwright/lib/runner/index.js:5473:23)
at Dispatcher._runJobInWorker (node_modules/playwright/lib/runner/index.js:5600:20) {
errno: -9,
code: 'EBADF',
syscall: 'spawn'
}
With 9,000 files instead of 11,000, the same project passes in UI mode.
Additional context
Cause. In UI mode, FSWatcher watches the project testDir (plus test dependencies) with the bundled chokidar. On macOS this keeps one open file descriptor per watched file. In our real project the UI-mode process held 10,658 open descriptors, about 10,600 of them regular files under testDir.
When a process holds more than macOS OPEN_MAX (10240) descriptors, the new stdio pipes for the worker get fd numbers above that limit. fork() with piped stdio then fails with EBADF. Raising ulimit -n doesn't help; ours is 1048576.
The same failure without Playwright:
// ebadf.cjs — node ebadf.cjs x 10200 → "ok"; node ebadf.cjs x 10300 → "EBADF"
const fs = require('fs');
const { fork } = require('child_process');
if (process.argv[2] === 'child') process.exit(0);
const n = Number(process.argv[3]);
for (let i = 0; i < n; i++) fs.openSync(__filename, 'r');
try {
const child = fork(__filename, ['child'], { stdio: ['ignore', 'pipe', 'pipe', 'ipc'] });
child.on('exit', () => console.log(n, 'ok'));
} catch (e) {
console.log(n, e.code);
}So any project with more than ~10k files under testDir cannot run tests in UI mode on macOS. Non-UI runs are not affected because they don't start the watcher.
Possible fixes.
- Use a watcher that does not hold one descriptor per file on macOS, such as FSEvents or recursive
fs.watch. - Watch only the test files and their dependencies, not every file under
testDir. - Let users exclude paths from the UI-mode watcher.
Workaround. Narrow testDir so it contains fewer than ~10k files.
Environment
System:
OS: macOS 27.0.1 (26A434), arm64
Binaries:
Node: 26.12.1
npmPackages:
@playwright/test: 1.64.1 (also 1.63.0)
Activity
github-actions commentedon Oct 27, 2026
github-actions commentedon Oct 27, 2026
Hi, I'm the Playwright bot and I reproduced this on macOS.
Reproduced on current main, Playwright
1.65.0-nextatc409f8c83ec04485e829809beceb722d9cf8ff38, with macOS 15.7.9 and Node.js 24.20.0. UI mode fails withspawn EBADFat 11,000 ordinary files; the same UI test passes at 9,000 files. An ordinary, non-UI run with 11,000 files also passes.The cause matches your report: the bundled chokidar 4.0.3 watches individual files throughout
testDir, including files that are not tests or dependencies. This can prevent Node from creating a worker with piped stdio.Narrowing
testDirremains a workaround. I also verified thatCHOKIDAR_USEPOLLING=1 npx playwright test --uiavoids this failure, but polling can increase CPU and filesystem activity; this is a chokidar setting, not a Playwright configuration option.Reproduction and checks
I extended the existing
should watch filestest intests/playwright-test/ui-mode-test-watch.spec.ts, adding this entry to itsrunUITest({...})file map:The existing test runs a test, edits it, and checks that watching reruns it successfully.
spawn EBADFinWorkerHost.startRunneron the initial attempt and first retry; later retries fail to update the test listCHOKIDAR_USEPOLLING=1EBADFfs.watchprobe with 11,000 filesCommands for the existing UI test:
The first command still made three retries because this spec sets retries in
test.describe.configure. The controls clearedCIto disable those retries. UI checks used Chromium, headless. No Firefox, WebKit, Linux, Windows, or published-version checks were needed to establish this live macOS process-spawning failure, and none were run.Fix direction
No fix is proposed in this run. Native recursive directory watching avoids the descriptor problem, but a direct replacement loses chokidar's symlink behavior in the probe above. A replacement needs to preserve dependency watching, discovery of new tests, ignored directories, and file replacement/deletion behavior. Enabling polling by default instead changes filesystem/CPU costs for every macOS UI session. This needs a watcher strategy decision rather than a guard around the failed spawn.
Triaged on macOS by the Playwright bot - agent run