Skip to content

[wrangler] Fix spawn EBADF when watching large assets directories on macOS - #16459

Closed
alfaroukhesham wants to merge 1 commit into
cloudflare:mainfrom
alfaroukhesham:fix/assets-watcher-ebadf-macos
Closed

alfaroukhesham wants to merge 1 commit into
cloudflare:mainfrom
alfaroukhesham:fix/assets-watcher-ebadf-macos

Conversation

@alfaroukhesham

@alfaroukhesham alfaroukhesham commented Oct 24, 2026 •

Copy link
Copy Markdown

Fixes #16264

wrangler dev on macOS exits with spawn EBADF once assets.directory holds more than about 10,000 files. Chokidar 4 opens one fs.watch (kqueue) handle per file; past macOS OPEN_MAX (10240) posix_spawn fails with EBADF (libuv/libuv#5204), so the existing EMFILE handling never runs.

On darwin and win32, watch the assets directory with a single recursive fs.watch (one FSEvents / ReadDirectoryChangesW handle for the tree). Linux keeps chokidar. Existing debounce, ignored write dirs (.wrangler, persist), error-to-warning handling, and teardown behaviour are unchanged.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:

Verified manually on macOS (Apple Silicon, Node 26) with a project whose assets directory holds 11,000 files: released wrangler 4.152.0 crashes with spawn EBADF on wrangler dev; with this fix wrangler dev starts, serves assets, hot reloads on asset edits, ignores changes under .wrangler/, and the wrangler process holds 43 open file descriptors with none per asset file.

  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: this is a bug fix to existing wrangler dev hot-reload behaviour; there is no new flag, config field, or user-facing API.

Note

This contribution was written with an AI coding agent and reviewed by me.

Open in Web Open in Cursor 

Devin Review

…macOS

On macOS, chokidar 4 opens one fs.watch handle per asset file. Past
OPEN_MAX, child-process spawns fail with EBADF and wrangler dev exits.
Watch the assets directory with a single recursive fs.watch on darwin
and win32, and keep chokidar on Linux.

Co-authored-by: alfaroukhesham <alfaroukhesham@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Oct 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8a080d1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
wrangler Patch
@cloudflare/vitest-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Hi @alfaroukhesham. Thanks for taking the time to contribute to Workers SDK.

This PR has been automatically closed because you are not currently a registered (vouched) collaborator for this repository.

AI tools now let our team generate proposed code changes quickly. Reviewing changes, checking that they solve the right problem, and maintaining code quality over time still take substantial care and effort. We therefore limit PRs to registered collaborators so we can focus that effort where it is most useful.

For the majority of external contributors, the most effective way to help is through issues: describing a problem or use case, providing a minimal reproduction, investigating its cause, or helping us verify a fix. Those contributions help us understand what needs to change and are very welcome.

We will take an initial look at this PR and decide whether it is something we want to invest time in reviewing and maintaining. We may reopen it if we decide to proceed, but we cannot guarantee that it will be reopened or receive a full review.

@github-actions github-actions Bot closed this Oct 24, 2026
@workers-devprod
workers-devprod requested review from a team and removed request for a team October 24, 2026 13:24
@github-project-automation github-project-automation Bot moved this from Untriaged to Done in workers-sdk Oct 24, 2026
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/assets-watcher-ebadf-macos.md: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/api/startDevWorker/BundlerController.assets-watcher.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/BundlerController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/watch-assets-directory.ts: [@cloudflare/wrangler]

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

Devin Review

Comment on lines +76 to +80
const filePath =
filename == null || filename === ""
? assetsDir
: path.resolve(assetsDir, filename.toString());
if (ignoredDirs.some((dir) => isPathInsideDir(filePath, dir))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Unnamed events reload ignored directories

When fs.watch omits a filename, writes under ignored directories trigger an assets refresh if assets include the project root. The fallback assetsDir never matches those ignored paths, so dev-session writes reload the Worker.

Learn more

Native filesystem watch events can omit the filename. When the assets directory is the project root, the project also contains write directories such as .wrangler; the ignored directory list normally prevents writes there from refreshing the Worker. For an unnamed event, replacing the unknown path with the assets root makes the ignore check ineffective, and the event calls the refresh listener. This can reload the Worker in response to its own development-session writes.

Example: With assets set to /app, a watch event from /app/.wrangler/tmp/output.js with filename === null becomes /app. The ignore check accepts it and emits another bundle refresh, unlike the same event with its filename present.

Recommended fix: Define a deliberate fallback for unnamed native events that avoids refreshing for writes in excluded directories while retaining real asset changes where possible. Consider whether a rescan or another watcher backend is necessary when the event cannot be attributed.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

const prefix = resolvedDir.endsWith(path.sep)
? resolvedDir
: resolvedDir + path.sep;
return resolvedTarget === resolvedDir || resolvedTarget.startsWith(prefix);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Windows persist writes trigger asset reloads

When a Windows persist folder differs in capitalization from dev.persist, isPathInsideDir does not ignore its events. Case-sensitive prefix checks treat the same Windows directory as a different path, so persist writes reload the Worker.

Learn more

The native watcher constructs a path from each filename and compares it against the ignored directory list. On Windows, paths differing only by capitalization refer to the same directory, but startsWith and strict equality compare them as distinct strings. The new native watcher therefore refreshes the bundle when Windows returns the actual directory casing instead of the spelling used in dev.persist.

Example: Set assets to C:\app and dev.persist to C:\app\persist-state; a real directory C:\app\Persist-State containing kv.sqlite produces an event for Persist-State\kv.sqlite. The event is not ignored even though it belongs to the configured persist directory.

Recommended fix: Normalize comparison paths using Windows-appropriate case-insensitive semantics for the win32 backend; preserve separator-boundary checks so sibling paths do not match.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.


Fix `wrangler dev` crashing with `spawn EBADF` on macOS when the assets directory contains many files

On macOS, chokidar 4 opens one `fs.watch` handle per asset file. Past the process file-descriptor limit this makes child-process spawns fail with `EBADF`, so `wrangler dev` exits. `wrangler dev` now watches the assets directory with a single recursive `fs.watch` on macOS and Windows, which uses one native handle for the whole tree.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changeset details obscure user benefit

The changeset explains watcher internals instead of focusing on wrangler dev users. Repository review rules require user-facing impact and discourage implementation details in changesets.

Suggested change
On macOS, chokidar 4 opens one `fs.watch` handle per asset file. Past the process file-descriptor limit this makes child-process spawns fail with `EBADF`, so `wrangler dev` exits. `wrangler dev` now watches the assets directory with a single recursive `fs.watch` on macOS and Windows, which uses one native handle for the whole tree.
Large assets directories no longer cause `wrangler dev` to exit unexpectedly on macOS. Asset changes still trigger hot reloads on macOS and Windows.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +83 to +85
for (const listener of allListeners) {
void listener("change", filePath);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Asset debug messages lose event detail

The native adapter reports every event as change. getAssetChangeMessage consequently labels additions and removals as generic changes, while the refresh path still handles them.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

🐛 BUG: wrangler dev dies with spawn EBADF on macOS when assets.directory has more than ~10,000 files

3 participants