deno
deno copied to clipboard
Deno exceeds OS open files limit (os error 24) when NodeJS does not
Some tools, notably project bundlers, work by observing a large number of files (typically the entire project folder). NodeJS can handle the OS limit on number of open files while Deno does not.
Please note:
- this limit can be system-wide (e.g., Linux), so programs can deplete this limit for each other and deno can fail intermittently and non-deterministically.
- this exception can not be caught by try-catch
Repro steps
demo.mjs
// Adjust this parameter if needed, the critical number will depend on your system state
const watcherCount = 100;
import { watch } from 'node:fs';
async function main() {
for (let i = 0; i < watcherCount; i++) {
console.log(`Creating watcher ${i}`);
watch('./a.txt', (d) => console.log(`'${d}' received by watcher ${i}`));
}
return new Promise(() => {});
}
await main();
Commands
- Create a file to watch:
touch a.txt - Run node with
node demo.mjsor deno withdeno run --allow-read=a.txt demo.mjsor both - Change file being watched:
echo 1 > a.txt - Observe that node prints out a bunch of change logs while deno fails
Error
error: Uncaught Error: Too many open files (os error 24)
iterator = Deno.watchFs(watchPath, {
^
at new FsWatcher (internal:runtime/js/40_fs_events.js:17:21)
at Object.watchFs (internal:runtime/js/40_fs_events.js:60:10)
at https://deno.land/[email protected]/node/_fs/_fs_watch.ts:119:21
at Object.action (internal:deno_web/02_timers.js:146:11)
at handleTimerMacrotask (internal:deno_web/02_timers.js:63:10)
I believe there are two errors here:
runtime/ops/fs_events.rscreates a new unique watcher for every call toDeno.watchFs(that isop_fs_events_open). deno runtime could create a single watcher on first call toop_fs_events_openand then reuse it for all subsequent calls.- Since
Watcher::new()may throw if OS limit is reached,Deno.watchFsprobably should capture the exception and relay it to the caller instead of crashing
- Since
Watcher::new()may throw if OS limit is reached,Deno.watchFsprobably should capture the exception and relay it to the caller instead of crashing
The error from Watcher::new() is being thrown to the user as an error. Your reproduction code just lacks a try-catch so the error becomes an uncaught error which closes the program.
The problem of reaching the open files limit is still valid, of course.
The error from Watcher::new() is being thrown to the user as an error. Your reproduction code just lacks a try-catch so the error becomes an uncaught error which closes the program.
I probably did not formulate this clearly enough. The above example does not have a try-catch, but when I add try-catch I still can not catch the error. For minimal repro case please consider this:
- Start deno in repl without permissions at all (to make sure
watchfails because of permissions):deno repl - Import
watch:import { watch } from 'node:fs'; - Call
watch()in a try-catch and deny permissions to ensurewatch()fails:try { watch('denied', () => {}) } catch (e) { console.log('not printed') } - Observe exception and program termination
Here is a repl run:
$ deno repl
Deno 1.30.3+0aeb8bc
exit using ctrl+d, ctrl+c, or close()
> import { watch } from 'node:fs';
undefined
> try { watch('denied', () => {}) } catch (e) { console.log('not printed') }
❌ Denied read access to "denied".
error: {"code":-32000,"message":"Execution was terminated"}
I'm not entirely sure, but this might be caused by setTimeout() in node:fs.watch(): https://github.com/denoland/deno/blob/main/ext/node/polyfills/_fs/_fs_watch.ts#L118
If I remove this setTimeout() and run the same test I get the expected behavior (error can be caught):
$ target/debug/deno repl
Deno 1.30.3
exit using ctrl+d, ctrl+c, or close()
> import { watch } from 'node:fs';
undefined
> try { watch('denied', () => {}) } catch (e) { console.log('not printed') }
❌ Denied read access to "denied".
not printed
undefined
I'm still observing both issues on Deno 1.32.3.
I think I'm also experiencing a related issue. I have a simple deno script that runs npm:tsc for typescript compilation and another deno script which uses npm:chokidar in conjunction with npm:prettier to watch files. The scripts are as follows:
tsc.sh:
exec deno run \
--allow-env \
--allow-read \
--allow-write \
'npm:[email protected]/tsc' \
--watch \
$@
format.sh:
#!/bin/bash
TOP=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# verify required binaries are available
for BINARY in deno; do
if ! type "$BINARY" > /dev/null 2>&1; then
echo "Required binary not found: $BINARY"
exit 1
fi
done
# start prettier formatter
deno run \
--allow-sys \
--allow-read \
--allow-env \
--allow-run \
"npm:chokidar-cli" \
"**/*.ts" \
"**/*.html" \
"**/*.css" \
-c "deno run \
--allow-read \
--allow-write \
--allow-env \
'npm:prettier' \
"${TOP}" --check --write"
I get the following error:
[4:25:13 PM] Starting compilation in watch mode...
[4:25:13 PM] Found 0 errors. Watching for file changes.
error: Uncaught Error: Too many open files (os error 24)
at new FsWatcher (ext:runtime/40_fs_events.js:17:21)
at Object.watchFs (ext:runtime/40_fs_events.js:60:10)
at ext:deno_node/_fs/_fs_watch.ts:58:21
at Object.action (ext:deno_web/02_timers.js:153:11)
at handleTimerMacrotask (ext:deno_web/02_timers.js:67:10)
at eventLoopTick (ext:core/01_core.js:189:21)
I have the same issue when watching files on vite+typescript project.
Solved by increasing inotify limit on my system (it's only 128 by default):
echo 10240 | sudo tee /proc/sys/fs/inotify/max_user_instances
I'm curious why it works out of the box on node.js though.
The call to Deno.watchFs is made in a setTimeout callback with no try/catch, so the exception can not be caught and the process crashes: https://github.com/denoland/deno/blob/61f1b8e8dc20846093a8b24a8f511a09bbf09919/ext/node/polyfills/_fs/_fs_watch.ts#L118-L124
Apparently the reason for the setTimeout is to avoid a race condition in the test. Perhaps the test could be improved to avoid the need for a kludge?
I'm curious why it works out of the box on node.js though.
Deno uses the inotify API (via notify-rs) under the hood on Linux whereas IIUC Node.js uses a (much slower) polling-based watching mechanism. Most software that uses inotify runs into this problem and increasing the OS limit is probably the best solution to this. I'll bring this up internally but I would much prefer to stick with OS APIs and ask users to increase the limit.
Btw, The error thrown should be catchable so that's a bug for sure.
I'm getting this while trying to run one of my SvelteKit projects with Vite. Curiously, it doesn't happen with the other projects I migrated to Deno...
I'm curious why it works out of the box on node.js though.
Deno uses the
inotifyAPI (via notify-rs) under the hood on Linux whereas IIUC Node.js uses a (much slower) polling-based watching mechanism. Most software that uses inotify runs into this problem and increasing the OS limit is probably the best solution to this. I'll bring this up internally but I would much prefer to stick with OS APIs and ask users to increase the limit.Btw, The error thrown should be catchable so that's a bug for sure.
@littledivy How large should the inotify limit be? Are there any issues one might run into if it's "too large"?
I tried running the following command based on what I cobbled together from the internet:
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
but I still get the same error.
EDIT: I wonder if it might be because due to an error on my side (at least in my case), because I am in the midst of upgrading a Svelte 3 project to a Svelte 5 project? I'll keep troubleshooting and update here when I do learn more.
I think there's some issue with chokidar and deno.
I turned off useFsEvents option for my svelte project, and am using usePolling to make it work for now.
it's really common to encounter this error in vite projects and forcing devs to change the inotify limits is far from ideal. and afaik it's still not even possible to catch the errors when using via fs.watch because of https://github.com/denoland/deno/issues/17757#issuecomment-2040161990, which packages like chokidar are looking out for.
I am facing this problem with Deno 2.0 in my small remix project: https://github.com/assertnotnull/remix-tiledhn
I am facing this problem too with sveltekit project. I have 81 files in my src folder and can't run dev task.
As @phoenisx mentioned, switch from useFsWatch to usePolling is good workaround for svelte :+1:
vite.config.js
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [sveltekit()],
server: {
watch: {
useFsEvents: false,
usePolling: true,
},
},
});
Same problem here with Deno 2.0.0. I am using chokidar watcher and I get this error message:
error: Uncaught Error: Too many open files (os error 24)
at new FsWatcher (ext:runtime/40_fs_events.js:24:17)
at Object.watchFs (ext:runtime/40_fs_events.js:74:10)
at ext:deno_node/_fs/_fs_watch.ts:57:21
at callback (ext:deno_web/02_timers.js:68:7)
at eventLoopTick (ext:core/01_core.js:210:13)
I tried with different amount of files and I noticed that there are some situations where it works without error (with fewer files). But this seems to be random. Sometimes the same amount of files work and in the next run it doesn't.
And it were not many files I tried with file sets between 1 and 75.
this issue seems to be resolved by canary/v2.0.3
I am having this exact issue in the latest version (v2.0.2), I am running Debian 12 (this issue seems to be linux specific)
$ deno task dev
Task dev remix vite:dev
➜ Local: http://localhost:5173/
➜ Network: http://10.40.7.220:5173/
➜ Network: http://100.97.243.22:5173/
➜ press h + enter to show help
error: Uncaught Error: Too many open files (os error 24)
at new FsWatcher (ext:runtime/40_fs_events.js:24:17)
at Object.watchFs (ext:runtime/40_fs_events.js:74:10)
at ext:deno_node/_fs/_fs_watch.ts:57:21
at callback (ext:deno_web/02_timers.js:68:7)
at eventLoopTick (ext:core/01_core.js:210:13)
I was hoping this ticket being closed would mean the issues was resolved but it was not
I have also tried adding the
useFsEvents: false,
usePolling: true,
from the svelte suggested solution but that did not have a noticible effect.
@nidx you can upgrade to canary to get this fix: deno upgrade canary, or wait for stable v2.0.3 which should be released tonight that will resolve the problem.