A Buffer is Node's answer to a problem JavaScript didn't originally have an answer for: how do you hold and manipulate raw binary data - file contents, network packets, image bytes - in a language whose only numeric type was a 64-bit float unsuited to byte-level work?
Buffers Basics shows the day-to-day API for allocating and slicing that data, and Text Encodings covers the specific case of converting bytes to and from strings. This page sits underneath both: what a Buffer actually is in memory, how it relates to the standard TypedArray family, and why "binary data" and "text" are two separate concerns that only meet at an explicit conversion step.
A Buffer is a fixed-length view over raw bytes stored outside V8's garbage-collected heap, exposed through an API that's a superset of the standard Uint8Array.
Insight: Binary data (network sockets, files, cryptography) doesn't fit a language built around dynamic, garbage-collected values - Buffer gives Node predictable, low-overhead storage for exactly that data.
When to Use: Reading/writing files or sockets at the byte level, implementing a binary protocol, hashing or encrypting data, or converting between text and bytes at a system boundary.
Limitations/Trade-offs: Buffers are fixed-size once allocated, live outside the heap V8's garbage collector optimizes for, and the fastest allocation path (allocUnsafe) can expose old memory contents if you're not careful.
Related Topics: TypedArrays and ArrayBuffer, text encoding (UTF-8), streams, binary protocol design.
Before Buffer existed, JavaScript had no good way to represent "give me exactly these 512 bytes, and let me read or write any one of them directly." Numbers were floats, strings were UTF-16 sequences, and neither maps cleanly onto a byte stream from a TCP socket or a file on disk.
Node introduced Buffer in its earliest versions to fill that gap: a fixed-length array of integers, each constrained to a single byte (0-255), backed by memory allocated outside the normal JavaScript heap. That "outside the heap" detail matters more than it first appears - it means large binary payloads don't pressure V8's garbage collector the way an equivalent array of JavaScript numbers would, and it means the memory has a raw, C-like layout that can be handed directly to system calls.
A useful analogy: think of a Buffer as a numbered row of storage lockers, each holding exactly one byte, with a fixed total count decided the moment the row is built. You can read or overwrite any locker by its number, you can hand a slice of consecutive lockers to something else without copying their contents, but you can't add a locker to the row or remove one - the row's length is fixed for its lifetime.
When JavaScript later standardized its own binary-data types - ArrayBuffer and the TypedArray family (Uint8Array, Int32Array, and so on) - Node adapted Buffer to sit on top of them rather than maintaining a wholly separate implementation.
Today, a Buffer is a subclass of Uint8Array, which is itself a view over a lower-level ArrayBuffer. That layering is the key mechanical fact this whole section rests on: the ArrayBuffer is the actual block of raw memory, and a TypedArray (including Buffer) is a lens that interprets some or all of that memory as a sequence of same-sized values - in Buffer's case, always single bytes.
Because Buffer is a Uint8Array under the hood, every standard TypedArray method works on it, and a Buffer can share the same underlying ArrayBuffer as a Uint8Array created elsewhere - mutating one through its view can be visible through the other, since they're windows onto the same memory rather than independent copies. Node's Buffer adds convenience on top: encoding-aware string conversion, integer read/write helpers at arbitrary byte offsets (readUInt32BE, writeInt16LE, and similar), and constructors tuned for Node's own use cases.
Encoding is the second mechanic worth separating clearly from the memory model: a Buffer never "is" UTF-8 or any other encoding - it's just bytes. Encoding is a rule applied at conversion time, telling Node how to interpret those bytes as text (or vice versa) when you explicitly ask.
const buf = Buffer.from('café', 'utf8'); // 5 bytes - é takes 2 bytes in UTF-8console.log(buf.length); // 5, not 4console.log(buf.toString('utf8')); // 'café' - only correct with matching encoding
That distinction explains a common surprise: buf.length counts bytes, not characters, and reading a multi-byte-encoded string with the wrong encoding argument produces silently wrong text rather than an error, because Buffer has no way to know which encoding you intended.
Allocation strategy is the third mechanic, and it's a direct memory-safety trade-off. Buffer.alloc(n) zero-fills its memory before returning it, guaranteeing no old data leaks through. Buffer.allocUnsafe(n) skips that zero-fill and instead recycles memory from an internal pre-allocated pool for speed - which means a freshly allocated unsafe Buffer can briefly contain whatever bytes were previously in that pool slot, until you overwrite them yourself. Buffer Security covers exactly when that trade-off is (and isn't) acceptable.
The relationship between Buffer, TypedArray, and the Web-standard binary APIs (TextEncoder, TextDecoder, Blob) has converged significantly as Node has adopted more of the browser's standard library alongside its own Node-specific one - which is why modern Node code increasingly mixes both freely rather than treating Buffer as a walled-off Node-only concept.
Approach
Strength
Weakness
Best Fit
Buffer
Node-native convenience methods (readUInt32BE, encoding-aware toString); zero extra dependency
Node-specific API surface; historically some footguns (unsafe allocation)
File I/O, sockets, Node-only binary protocol code
Plain Uint8Array / ArrayBuffer
Web-standard, portable to browsers and other JS runtimes unchanged
Fewer built-in convenience methods for byte-level manipulation
Code meant to run in both Node and the browser
TextEncoder / TextDecoder
Web-standard, explicit and predictable UTF-8 handling with error modes
Text-only - not a general binary data container
Converting strictly between UTF-8 text and bytes
At scale, the memory-outside-the-heap property becomes an operational concern rather than just a performance footnote: large numbers of long-lived Buffers reduce pressure on V8's garbage collector, but they still consume process memory that monitoring tools need to account for separately from typical heap metrics. That's part of why loading an entire large file into one giant Buffer is usually the wrong call - Streams Basics covers processing the same data in bounded chunks instead of holding it all in memory at once.
Security-sensitive code has its own sharp edge here: because allocUnsafe recycles pool memory, code that allocates an unsafe Buffer and only partially fills it before sending it somewhere (a socket, a response body) can leak fragments of unrelated previous data. Buffer Security covers this and related concerns like timing-safe comparison for cryptographic byte data. Binary Protocols builds on the read/write-at-offset mechanics described above to parse and construct real wire formats.
"A Buffer stores text with an encoding attached to it." A Buffer only ever stores bytes - encoding is a conversion rule applied when you explicitly convert to or from a string, not a property the bytes carry with them.
"buf.length gives you the character count of the text it holds." It gives you the byte count - for any encoding using more than one byte per character (UTF-8's non-ASCII characters, for example), those numbers diverge.
"Buffer and Uint8Array are unrelated, competing APIs." Buffer is a Uint8Array subclass - the same underlying bytes, with extra Node-specific convenience methods layered on top, not a separate data type.
"Buffer.allocUnsafe is just a faster, equivalent version of Buffer.alloc." It skips zero-filling entirely, so the returned memory can briefly contain leftover data from prior use - it's faster specifically because it trades away that guarantee, not a free upgrade.
"Slicing a Buffer copies its data."buf.subarray() (and legacy buf.slice()) return a view over the same underlying memory by default - writing through the slice can mutate the original Buffer's bytes too.
A fixed-length sequence of single-byte values backed by memory allocated outside V8's garbage-collected JavaScript heap, exposed to your code through an API that's a superset of the standard Uint8Array.
Why didn't Node just use regular JavaScript arrays for binary data?
Regular arrays hold arbitrary JavaScript values (floats by default) with dynamic length and heap-managed memory - none of which suits raw, fixed-size, byte-constrained binary data efficiently, so Node needed a purpose-built type before JavaScript had TypedArrays of its own.
How does Buffer relate to `ArrayBuffer` and `TypedArray`?
ArrayBuffer is the raw memory block; a TypedArray (like Uint8Array) is a typed view over some or all of that memory; Buffer is Node's own subclass of Uint8Array, adding convenience methods while remaining fully compatible with the standard TypedArray machinery.
Does a Buffer know what text encoding it contains?
No - a Buffer is just bytes with no attached metadata about encoding; you supply the encoding explicitly every time you convert to or from a string (buf.toString('utf8'), Buffer.from(str, 'utf8')), and Node trusts whatever encoding you name.
Why is `buf.length` sometimes different from the string's character count?
Because buf.length counts bytes, and encodings like UTF-8 use a variable number of bytes per character - ASCII characters take one byte, but many non-ASCII characters take two, three, or four, so byte count and character count only match for pure ASCII content.
What's the actual difference between `Buffer.alloc` and `Buffer.allocUnsafe`?
Buffer.alloc(n) zero-fills the memory before returning it, guaranteeing clean bytes at a small performance cost; Buffer.allocUnsafe(n) skips that step and recycles memory from an internal pool for speed, which means the returned bytes can briefly hold unrelated leftover data until you overwrite them.
Is slicing a Buffer expensive?
No, and that's precisely because it's cheap that it can surprise you - subarray()/slice() return a view over the same memory rather than copying it, so the operation itself is fast, but writes through the slice affect the original Buffer too.
Should I load a whole file into a Buffer, or use a stream?
For small, bounded files, a single Buffer is simple and fine; for large or unbounded input, a stream processes the data in fixed-size chunks instead of holding the entire payload in memory at once, which keeps memory usage predictable regardless of input size.
Are Buffer and the Web-standard `TextEncoder`/`TextDecoder` doing the same job?
They overlap for the text-conversion case specifically - TextEncoder/TextDecoder handle UTF-8 text-to-bytes conversion in a Web-standard, portable way, while Buffer is a general-purpose binary container with a much broader API beyond just text.
Can mutating a `Uint8Array` affect a Buffer, or vice versa?
Yes, if they share the same underlying ArrayBuffer - since Buffer is a Uint8Array subclass and both are just views over memory, two views over the same block see each other's writes, they aren't independent copies.
Why does Node keep a memory pool for Buffer allocation?
Allocating small buffers one at a time from the operating system has real per-allocation overhead; pooling pre-reserves a larger block and hands out slices of it for small allocations, which is faster but is exactly the mechanism that makes allocUnsafe's stale-memory risk possible.