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
- The process hangs indefinitely - after the EPERM, Vitest does not exit or provide actionable guidance. The user must manually kill the process.
- The 5s timeout is too limiting - for projects with large dependency graphs, the browser-mode server consistently exceeds this window on Windows.
- No configuration option - users cannot increase the timeout without patching Vite source.
Suggested fixes
-
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;
-
Don't hang on failure - if the rename ultimately fails, exit with a clear error message and actionable suggestions instead of hanging forever.
-
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.
-
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:
npm install && npx playwright install chromium
rm -rf node_modules/.vite (ensure clean cache)
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
Describe the bug
Describe the bug
Vitest browser mode fails with
EPERM: operation not permittedon Windows when a project's dependencies generate a large number of pre-bundled chunks. Vite's dep optimizer tries to renamedeps_temp_*todeps, 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-highlighterwith 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 thesafeRenameexhausts its timeout and fails.Vite already has a Windows-specific retry mechanism (
safeRenamewithGRACEFUL_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
--browser.enabled=false(Windows)GRACEFUL_RENAME_TIMEOUTto 30soptimizeDeps.noDiscovery: true+include: []optimizeDeps.force: true--no-sandboxlaunch argsRoot cause analysis
depstarget directory does not exist at rename time (onlydeps_temp_*exists), so the lock is on the source directory or its contents--browser.enabled=falseworks, the handle holder is likely Vite's internal browser-mode dev server serving pre-bundled files to the browser before the rename completesreact-syntax-highlightergenerates 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 windowIssues with current behavior
Suggested fixes
Increase
GRACEFUL_RENAME_TIMEOUTfrom5e3(5s) to at least30e3(30s), or make it configurable via an environment variable or Vite config option. The constant is insrc/node/optimizer/index.ts:Don't hang on failure - if the rename ultimately fails, exit with a clear error message and actionable suggestions instead of hanging forever.
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.Provide a direct-write mode - allow the optimizer to write directly to the
depsdirectory (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 existingdepsdirectory to protect. Writing directly todepsin these cases would bypass the rename entirely.Environment
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) plusreact-syntax-highlighterreliably 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:
src/App.tsx:
src/App.test.tsx:
Steps:
npm install && npx playwright install chromiumrm -rf node_modules/.vite(ensure clean cache)npx vitest runResult: EPERM error, process hangs indefinitely:
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.10Used Package Manager
npm
Validations