Title: fs: caller-supplied buffer for fs.readFile / fs.readFileSync · Issue #63600 · nodejs/node · GitHub
Open Graph Title: fs: caller-supplied buffer for fs.readFile / fs.readFileSync · Issue #63600 · nodejs/node
X Title: fs: caller-supplied buffer for fs.readFile / fs.readFileSync · Issue #63600 · nodejs/node
Description: fs.readFile* allocates a fresh Buffer on every call. Fine for most code, expensive on hot paths. Multiple Worker threads doing concurrent reads contend hard on the per-process mmap_lock in the kernel — about 17× longer wait per allocatio...
Open Graph Description: fs.readFile* allocates a fresh Buffer on every call. Fine for most code, expensive on hot paths. Multiple Worker threads doing concurrent reads contend hard on the per-process mmap_lock in the kern...
X Description: fs.readFile* allocates a fresh Buffer on every call. Fine for most code, expensive on hot paths. Multiple Worker threads doing concurrent reads contend hard on the per-process mmap_lock in the kern...
Opengraph URL: https://github.com/nodejs/node/issues/63600
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"fs: caller-supplied buffer for fs.readFile / fs.readFileSync","articleBody":"`fs.readFile*` allocates a fresh Buffer on every call. Fine for most code, expensive on hot paths. Multiple Worker threads doing concurrent reads contend hard on the per-process `mmap_lock` in the kernel — about 17× longer wait per allocation than equivalent child processes (trace in #63597).\n\nThe workaround today is `openSync` + `readSync` + `closeSync` with your own buffer. Works, but you lose `readFile`'s ergonomics, especially for the async variants.\n\nTwo small additions would close that gap.\n\n### `{ buffer }`\n\nPass a buffer to fill, get back a `subarray` view of the actual file bytes:\n\n```js\nimport fs from 'node:fs';\nimport fsp from 'node:fs/promises';\n\nconst buf = Buffer.allocUnsafe(1 \u003c\u003c 20);\n\n// sync\nconst data = fs.readFileSync(path, { buffer: buf });\n// data === buf.subarray(0, fileSize)\n\n// promise\nconst data2 = await fsp.readFile(path, { buffer: buf });\n\n// callback\nfs.readFile(path, { buffer: buf }, (err, data3) =\u003e { /* ... */ });\n```\n\nIf the file is bigger than the buffer, throw. Same shape as `crypto.randomFillSync(buf)` vs `crypto.randomBytes(n)`, or `fs.read(fd, buf, ...)` at the lower level.\n\n### `{ getBuffer(size) }`\n\nPass a factory called with the file's size (we already get it from `fstat`):\n\n```js\nimport fs from 'node:fs';\nimport fsp from 'node:fs/promises';\n\nconst opts = { getBuffer(size) { return pool.acquire(size); } };\n\nconst data = fs.readFileSync(path, opts);\nconst data2 = await fsp.readFile(path, opts);\nfs.readFile(path, opts, (err, data3) =\u003e { /* ... */ });\n```\n\nFor pool implementations that want to pick the right slab once they know the size, instead of pre-allocating one giant buffer per concurrent caller.\n\n`getBuffer` is synchronous in all variants. The async forms still benefit because the allocation step happens inline before the libuv read kicks off.\n\n### Why both\n\n`{ buffer }` is for the simple case (one reusable buffer per worker or stream). `{ getBuffer }` is for pool implementations. They compose:\n\n```\nbuffer → use it\ngetBuffer → call it\nneither → allocate (today's behaviour, unchanged)\n```\n\nPassing both throws.\n\n### Things to figure out\n\n* `encoding` + `buffer`: I'd say return the string like today, treat the buffer as scratch.\n* Pool release: leave to userland for v1. The caller can wrap the returned subarray however they want.\n\nImplementation order would be `readFileSync` first, then `fsPromises.readFile`, then callback-style `fs.readFile`.\n\ncc @nodejs/fs @nodejs/performance","author":{"url":"https://github.com/mcollina","@type":"Person","name":"mcollina"},"datePublished":"2026-05-27T12:23:29.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":2},"url":"https://github.com/63600/node/issues/63600"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:6552379e-e178-45dc-9d79-c41439761d13 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | CE08:15305D:733521:9C87F5:6A4D4615 |
| html-safe-nonce | df7f9cd155dbb3705c9856069cf7dfa8710be95de4d4939557d56e1404587ff8 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDRTA4OjE1MzA1RDo3MzM1MjE6OUM4N0Y1OjZBNEQ0NjE1IiwidmlzaXRvcl9pZCI6IjI5Njc3MjkzNzg2MjI3ODUwNDUiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | c74304facef09ab52384b8e8be4499b3566301306b71fe17f670f69907fdd219 |
| hovercard-subject-tag | issue:4532268581 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/63600/issue_layout |
| twitter:image | https://opengraph.githubassets.com/eff33af372a2bf83fe0600e286225a6115ce236af1607d2b69c149c51f81b1fd/nodejs/node/issues/63600 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/eff33af372a2bf83fe0600e286225a6115ce236af1607d2b69c149c51f81b1fd/nodejs/node/issues/63600 |
| og:image:alt | fs.readFile* allocates a fresh Buffer on every call. Fine for most code, expensive on hot paths. Multiple Worker threads doing concurrent reads contend hard on the per-process mmap_lock in the kern... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | mcollina |
| hostname | github.com |
| expected-hostname | github.com |
| None | 92571a8944142227b7e19cd10918b1ddd06e5066c1ad5bc7e4769cf6140a87e6 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 56fc8347865a14e2ec811533d68f929cf4e0ec19 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width