Skip to content

buffer: increase Buffer.poolSize default to 64 KiB - #63597

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
mcollina:buffer-poolsize-64kb
May 29, 2026
Merged

buffer: increase Buffer.poolSize default to 64 KiB#63597
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
mcollina:buffer-poolsize-64kb

Conversation

@mcollina

@mcollina mcollina commented May 27, 2026

Copy link
Copy Markdown
Member

The Buffer.poolSize default of 8 KiB has been unchanged since May 2015. Two issues with it today:

  1. The 4 KiB cliff. The pool check is size < (Buffer.poolSize >>> 1), so with the 8 KiB default the threshold is 4 KiB and the strict inequality means a 4 KiB allocation itself bypasses the pool. The current default helps allocations from 1 B to 3999 B and abruptly stops at the page-aligned and HTTP-frame-sized boundary where many real allocations land.

  2. Stale in 2025. Predates HTTP/2 (16 KiB-1 MiB frame sizes), modern stream chunk sizes, and ~10× growth in typical RAM. Many Buffer.allocUnsafe calls in core (fs.readFileSync for non-utf8 reads, HTTP parser, stream chunkers) sit in the 4-64 KiB range and miss the pool.

This PR raises the default to 64 KiB, extending pool coverage to allocations up to ~32 KiB.

Evidence

Benchmark setup: 8 Worker threads in one process, each looping fs.readFileSync(file) on files of various sizes, measuring throughput. Linux 6.8, glibc 2.39, i7-7700, Node main (built locally).

File size 8 KiB pool (default) 64 KiB pool Δ
512 B 404k ops/s 431k ops/s +7%
2 KiB 360k ops/s 367k ops/s ~
4 KiB 326k ops/s 360k ops/s +10%
8 KiB 202k ops/s 254k ops/s +26%
16 KiB 148k ops/s 181k ops/s +23%
64 KiB 86k ops/s 87k ops/s ~
1 MiB 12k ops/s 12k ops/s ~

Wins where it matters (4-32 KiB), no regressions at small or large sizes.

Why workers benefit more than single-threaded

Buffer.allocUnsafe(size) for size ≥ Buffer.poolSize/2 falls through to fresh V8 ArrayBuffer allocations, which land on glibc malloc. When multiple Worker threads do this concurrently they contend on the per-mm_struct mmap_lock write lock (every arena growth takes it). Confirmed by bpftrace: workers wait ~17× longer per mmap_lock acquisition than equivalent child processes on the same workload, with cumulative wait dropping ~2.5× when the pool covers the allocation size. Full investigation: https://github.com/platformatic/node-worker-mmap-lock-contention.

Single-threaded apps benefit too, just less dramatically — fewer allocator round-trips for medium-sized buffers.

Cost

+56 KiB RSS per realm at startup (one 64 KiB pool per realm; main thread + each Worker thread). On a typical app with 1 main + 4 Workers that's +280 KiB, trivial on modern hardware. The pool occupies RSS for the lifetime of any sliced Buffer that's still referenced, so peak RSS may grow modestly in apps that hold many small Buffers; same shape as today, just larger granularity.

Why 64 KiB and not larger

Tested 32 / 64 / 128 / 256 KiB; 64 KiB is the smallest pool that captures the 4-16 KiB hot zone where the current default fails. 128/256 KiB give additional wins on 16-64 KiB allocations (+64% at 16 KiB with a 256 KiB pool) but the marginal RSS cost is harder to justify as a default. Easy follow-up if there's appetite.

Pool refills at 64 KiB stay under glibc's M_MMAP_THRESHOLD (128 KiB default), so each refill uses the heap rather than triggering a fresh mmap — that boundary is part of why 64 KiB is the right ceiling for a default.

Notes

  • Doc updated in doc/api/buffer.md (default value + the "4 KiB" example reference).
  • No code change other than the constant; the existing Buffer.poolSize setter machinery is unchanged.
  • Filed as draft for discussion of the value (64 KiB vs 128 KiB) and to surface any concerns about RSS in memory-constrained deployments (Lambda, embedded).

cc @nodejs/buffer @nodejs/performance