Skip to content

Browser mode: EPERM hang on Windows when dependencies produce many pre-bundled chunks #10890

Description

@aarowman

Describe the bug

Describe the bug

Vitest browser mode fails with EPERM: operation not permitted on Windows when a project's dependencies generate a large number of pre-bundled chunks. Vite's dep optimizer tries to rename deps_temp_* to deps, but the browser-mode dev server holds file handles on the chunks longer than the 5-second retry timeout allows. The process hangs indefinitely with no recovery. This occurs regardless of browser provider (tested with both Playwright and the preview provider).

The root cause is that dependencies generating a large number of pre-bundled chunks (e.g., react-syntax-highlighter with hundreds of language grammar files) cause the browser-mode dev server to hold open file handles for longer than Vite's 5-second retry window. On Windows, open file handles block directory renames, so the safeRename exhausts its timeout and fails.

Vite already has a Windows-specific retry mechanism (safeRename with GRACEFUL_RENAME_TIMEOUT = 5000ms), but 5 seconds is insufficient for projects with large chunk counts. Increasing the timeout to 30 seconds resolves the issue - the rename eventually succeeds, indicating the file handles are released after 5-30 seconds.

Key findings

Scenario Result
Browser mode + MUI stack + react-syntax-highlighter (Windows) EPERM, hangs
Browser mode + MUI stack only (Windows) Works fine
Browser mode + react-syntax-highlighter only (Windows) Works fine
--browser.enabled=false (Windows) Works fine
Browser mode (Linux/CI) Works fine
Increase GRACEFUL_RENAME_TIMEOUT to 30s Works fine
Move cache to OS temp dir Still EPERM
Move cache outside node_modules Still EPERM
Remove all IDEs (Git Bash and CMD, no IDE running) Still EPERM
optimizeDeps.noDiscovery: true + include: [] No EPERM, but EMFILE (too many open files) and CJS deps break
optimizeDeps.force: true Still EPERM
Playwright --no-sandbox launch args Still EPERM
Preview provider (local browser, not Playwright) Still EPERM

Root cause analysis

  • The deps target directory does not exist at rename time (only deps_temp_* exists), so the lock is on the source directory or its contents
  • Since --browser.enabled=false works, the handle holder is likely Vite's internal browser-mode dev server serving pre-bundled files to the browser before the rename completes
  • The handle is released after 5-30 seconds (30s timeout workaround succeeds)
  • The issue is volume-dependent: react-syntax-highlighter generates hundreds of language grammar chunk files during pre-bundling, and combined with MUI's chunk count, the total exceeds the server's handle-release time within the 5s window
  • Reproducible on multiple machines (different IDEs, and with no IDE running)
  • Corporate Windows 11 environment (non-admin, cannot modify security policies)

Issues with current behavior

  1. The process hangs indefinitely - after the EPERM, Vitest does not exit or provide actionable guidance. The user must manually kill the process.
  2. The 5s timeout is too limiting - for projects with large dependency graphs, the browser-mode server consistently exceeds this window on Windows.
  3. No configuration option - users cannot increase the timeout without patching Vite source.

Suggested fixes

  1. Increase GRACEFUL_RENAME_TIMEOUT from 5e3 (5s) to at least 30e3 (30s), or make it configurable via an environment variable or Vite config option. The constant is in src/node/optimizer/index.ts:

    const GRACEFUL_RENAME_TIMEOUT = 5e3;
  2. Don't hang on failure - if the rename ultimately fails, exit with a clear error message and actionable suggestions instead of hanging forever.

  3. Investigate the server handle race - the browser-mode dev server should not hold file handles on deps_temp_* contents during the rename window. If the server could defer reading from the new bundle until after the rename completes, the race would be eliminated.

  4. Provide a direct-write mode - allow the optimizer to write directly to the deps directory (no temp directory + rename at all). The atomic rename pattern exists to prevent serving partially-written files, but when the cache doesn't exist yet, there's no existing deps directory to protect. Writing directly to deps in these cases would bypass the rename entirely.

Environment

  • Vitest: 4.1.5
  • Vite: 7.3.2
  • @vitest/browser: 4.1.5
  • @vitest/browser-playwright: 4.1.5
  • Playwright: 1.58.2
  • Node.js: 22.16.0
  • OS: Windows 11 (corporate, non-admin)
  • Shell: Git Bash (also reproduces in CMD and PowerShell)

Reproduction

Reproduction

Minimal reproduction requires a project with enough dependencies to generate a large number of pre-bundled chunks. The combination of the MUI stack (@mui/material, @mui/icons-material, @emotion/react, @emotion/styled) plus react-syntax-highlighter reliably triggers it on Windows. Neither MUI alone nor react-syntax-highlighter alone is sufficient - it's the combined volume of chunks that pushes past the 5s retry window.

The issue occurs with any browser provider (Playwright or preview) - the repro uses Playwright but results are identical with @vitest/browser-preview.

package.json:

{
  "name": "vitest-eperm-repro",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "vitest run"
  },
  "devDependencies": {
    "@emotion/react": "^11.11.1",
    "@emotion/styled": "^11.11.0",
    "@mui/icons-material": "^5.11.16",
    "@mui/material": "^5.13.6",
    "@testing-library/react": "^16.3.2",
    "@vitejs/plugin-react": "^4.4.1",
    "@vitest/browser": "^4.1.5",
    "@vitest/browser-playwright": "^4.1.5",
    "playwright": "^1.58.2",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-syntax-highlighter": "^15.6.6",
    "vite": "^7.3.1",
    "vitest": "^4.1.5"
  }
}

vite.config.ts:

import react from '@vitejs/plugin-react';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [react()],
  test: {
    projects: [
      {
        optimizeDeps: {
          include: ['react', 'react-dom', 'react/jsx-dev-runtime'],
        },
        test: {
          name: 'unit',
          include: ['src/**/*.test.{ts,tsx}'],
          browser: {
            enabled: true,
            headless: true,
            provider: playwright({}),
            instances: [{ browser: 'chromium' }],
          },
        },
      },
    ],
  },
});

src/App.tsx:

import { Home } from '@mui/icons-material';
import { Button } from '@mui/material';
import { useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';

export function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>Counter: {count}</h1>
      <Button startIcon={<Home />} onClick={() => setCount((c) => c + 1)}>
        Increment
      </Button>
      <SyntaxHighlighter language="javascript">{'const x = 1;'}</SyntaxHighlighter>
    </div>
  );
}

src/App.test.tsx:

import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
import { App } from './App';

test('renders counter', () => {
  render(<App />);
  expect(screen.getByText('Counter: 0')).toBeDefined();
});

Steps:

  1. npm install && npx playwright install chromium
  2. rm -rf node_modules/.vite (ensure clean cache)
  3. npx vitest run

Result: EPERM error, process hangs indefinitely:

[vite] (client) error while updating dependencies:
Error: EPERM: operation not permitted, rename
  '...\node_modules\.vite\vitest\<hash>\deps_temp_<id>'
  -> '...\node_modules\.vite\vitest\<hash>\deps'

System Info

System:
    OS: Windows 11 10.0.26200
    CPU: (28) x64 13th Gen Intel(R) Core(TM) i7-13850HX
    Memory: 34.09 GB / 63.69 GB
  Binaries:
    Node: 22.16.0 - C:\Program Files\nodejs\node.EXE
    npm: 10.9.2 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Chrome: 151.0.7922.109
    Edge: Chromium (150.0.4078.48)
    Firefox: 140.12.0 - C:\Program Files\Mozilla Firefox\firefox.exe
    Internet Explorer: 11.0.26100.8115
  npmPackages:
    @vitejs/plugin-react: ^4.4.1 => 4.7.0
    @vitest/browser: ^4.1.5 => 4.1.10
    @vitest/browser-playwright: ^4.1.5 => 4.1.10
    @vitest/browser-preview: ^4.1.10 => 4.1.10
    playwright: ^1.58.2 => 1.62.0
    vite: ^7.3.1 => 7.3.6
    vitest: ^4.1.5 => 4.1.10

Used Package Manager

npm

Validations

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions