Repository navigation
Close listener leak in fs/promises createReadStream #64214
Description
Activity
The workaround is great!
I was thinking, should there be
stream.once('close', teardown)inif (after.length > before) { const listener = after[after.length - 1]; const teardown = () => { // teardown code }; //-->here<--- stream.once('end', teardown); stream.once('error', teardown); } return stream;
since if stream is destroyed early and listener stays attached, it may cause leak.
- added a commit that references this issue
on Aug 17, 2026 Re-opening, since #64227 was deemed to be broken (see #64227 (comment)).
#65387 reverts the fix, and includes #65387 (comment):
Proper fix is to use eos/finished instead of 'close' listener
- added a commit that references this issue
on Aug 19, 2026 - addedfsIssues and PRs related to file-system APIs and the fs module.Issues and PRs related to file-system APIs and the fs module.
on Aug 21, 2026 @ronag could you clarify your comment on the correct way to fix this please?
Proper fix is to use eos/finished instead of 'close' listener
I'd be happy to try putting together a PR myself to resolve this, but I don't understand what you mean by "use eos/finished"; I believe the existing code (which uses the
closelistener on the file descriptor) exists to shut down all remaining streams if the descriptor is closed while they are still active, which I think requires (?) listening forcloseon the file descriptor object (but, ideally, tidying up that listener when the stream completes). I think I must be misunderstanding something?(I'm also not sure what execution path in Y1D7NG's now-reverted PR would cause it to unref the handle multiple times, but I don't doubt there are safer / more elegant ways to solve this)
- added 4 commits that reference this issue
on Aug 25, 2026 Since this issue is lingering somewhat, I'll share my latest user-space workaround specifically for read streams which seems to be reliably avoiding any problems in my own code. It's messy but it works, and includes various guards to avoid crazy behaviour if Node.js' internals change in the future. It's an iteration on the code I posted as a workaround above, but ensures the handler is unref'd (via
stream.destroy) to avoid hanging:export function createSafeReadStream(handle, options) { // this helper is a workaround for https://github.com/nodejs/node/issues/64214 const wrappedHandle = handle.close && options.autoClose === false ? new Proxy(handle, { // stream must be destroyed on end, but will call handler.close, which we do not want. // swap out handler.close for the stream to prevent this get: (target, p, ...rest) => p === 'close' ? () => Promise.resolve() : Reflect.get(target, p, ...rest), }) : handle; const before = handle.listeners('close').length; const stream = wrappedHandle.createReadStream(options); const after = handle.listeners('close'); const listener = after.length > before ? after[after.length - 1] : undefined; const onEnd = () => { if (listener) { // the close listener is not removed by Node.js, so we must remove it manually handle.off('close', listener); } // if we do not call destroy on the stream, the eventual call to handler.close will hang stream.destroy(); stream.off('end', onEnd); stream.off('error', onError); }; const onError = () => { handle.close?.(); onEnd(); }; stream.once('end', onEnd); stream.once('error', onError); return stream; }
Also available with TypeScript definitions and some extra guards: https://github.com/davidje13/web-listener/blob/main/src/util/createSafeReadStream.mts
- added 2 commits that reference this issue
on Aug 27, 2026 - added 2 commits that reference this issue
on Sep 8, 2026 - added 2 commits that reference this issue
on Sep 17, 2026
Version
v24.18.0
Platform
Subsystem
fs/promises
What steps will reproduce the bug?
Here is a silly example which prints the first 11 bytes of a file by reading them separately from disk:
Running it (pointing at any file containing at least 11 bytes) will demonstrate the issue (output below)
How often does it reproduce? Is there a required condition?
Every call to
createReadStreamadds a close listener to the file handle, and I have not found a way to remove this listener. If called at least 11 times, it will trigger Node.js' built-in event leak detection warning. The threshold can be increased to avoid this warning, but the leak remains.What is the expected behavior? Why is that the expected behavior?
Once a stream is consumed, the
closeevent listener it attaches to theFileHandleshould be removed, even whenautoCloseis false, so that applications can read arbitrarily many ranges from a file.What do you see instead?
Additional information
This can be worked around in user-space with a hacky approach: