Buffer Basics
JavaScript strings are great for text, but a huge amount of real-world work involves raw bytes: reading files, talking to sockets, hashing data, parsing protocols, or handling image and audio payloads. Node.js exposes a Buffer class for exactly this — a fixed-length sequence of bytes that lives outside V8’s managed heap. Understanding how to create, size, and index buffers is the foundation for every binary-data task in Node.
Why buffers exist
V8 (the JavaScript engine) was built to manage strings, numbers, and objects on a garbage-collected heap. It has no native concept of “a contiguous block of raw bytes,” and copying large binary payloads in and out of the JS heap on every I/O operation would be slow and memory-hungry.
Buffer solves this by allocating memory off the V8 heap (memory the engine doesn’t have to scan during garbage collection) while still presenting a friendly, array-like JavaScript interface. When you read from a file or a TCP socket, Node hands you a Buffer pointing at that off-heap memory — no expensive conversion required. Under the hood, Buffer is a subclass of Uint8Array, so everything you know about typed arrays applies here too.
Bufferis a global in Node.js — you don’t need to import it. You can import it explicitly withimport { Buffer } from 'node:buffer';, which is good practice in shared libraries that document their dependencies.
Creating buffers
There are three primary ways to create a buffer, and choosing the right one matters for both correctness and security.
Buffer.from — wrap or copy existing data
Buffer.from() builds a buffer from something you already have: a string, an array of byte values, or another buffer/typed array.
// From a string (UTF-8 by default)
const hello = Buffer.from('hello');
// From a string with an explicit encoding
const fromHex = Buffer.from('48656c6c6f', 'hex');
const fromB64 = Buffer.from('aGVsbG8=', 'base64');
// From an array of byte values (0–255)
const bytes = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
console.log(hello.toString()); // decode back to text
console.log(fromHex.toString());
console.log(bytes.toString());
Output:
hello
Hello
Hello
Buffer.alloc — safe, zero-filled allocation
When you need an empty buffer of a known size to fill in yourself, use Buffer.alloc(size). It returns memory that is zero-filled, so you never accidentally read leftover data.
const buf = Buffer.alloc(8);
console.log(buf);
Output:
<Buffer 00 00 00 00 00 00 00 00>
You can pass an optional fill value and encoding: Buffer.alloc(4, 1) produces <Buffer 01 01 01 01>.
Buffer.allocUnsafe — fast but uninitialized
Buffer.allocUnsafe(size) skips the zero-filling step, so it’s faster — but the returned memory may contain old, leftover bytes from previously freed buffers. That’s a real security and correctness hazard if you forget to overwrite every byte before using the buffer.
const fast = Buffer.allocUnsafe(8);
console.log(fast); // contents are unpredictable!
// Only safe if you immediately overwrite the whole thing:
fast.fill(0);
Only use
allocUnsafewhen you will write to every byte before the buffer is read or exposed, and you’ve measured that allocation is a real bottleneck. When in doubt, useBuffer.alloc.
Which constructor to use
| Method | Zero-filled? | Speed | Use when |
|---|---|---|---|
Buffer.from(data) | N/A (copies/wraps data) | Fast | You already have bytes, a string, or an array |
Buffer.alloc(size) | Yes | Slower | You need a blank, safe buffer of a fixed size |
Buffer.allocUnsafe(size) | No | Fastest | Hot path and you overwrite every byte first |
Note that the old new Buffer(...) constructor is deprecated and unsafe (its behavior depended on the argument type). Always use the Buffer.from / Buffer.alloc factory methods instead.
Length and indexing bytes
A buffer’s .length property reports its size in bytes — not characters. Because multi-byte characters (like emoji or accented letters) encode to more than one byte in UTF-8, the byte length can exceed the string’s character count.
const text = Buffer.from('héllo'); // 'é' is 2 bytes in UTF-8
console.log('byte length:', text.length);
console.log('char length:', 'héllo'.length);
Output:
byte length: 6
char length: 5
Since Buffer is array-like, you can read and write individual bytes with bracket notation. Each element is an integer from 0 to 255.
const buf = Buffer.from('ABC');
console.log(buf[0]); // 65 — the byte value of 'A'
buf[0] = 0x5a; // overwrite first byte with 'Z'
console.log(buf.toString()); // 'ZBC'
// Iterate over bytes
for (const byte of buf) {
console.log(byte);
}
Output:
65
ZBC
90
66
67
Assigning a value outside 0–255 wraps modulo 256 (e.g. buf[0] = 256 stores 0), and indexes past the end are silently ignored — buffers do not grow. To compute how many bytes a string will occupy without allocating, use Buffer.byteLength(str, encoding).
console.log(Buffer.byteLength('héllo')); // 6
console.log(Buffer.isBuffer(Buffer.alloc(1))); // true
Best Practices
- Default to
Buffer.allocfor new buffers; reach forallocUnsafeonly after measuring and only when you overwrite every byte. - Never use the deprecated
new Buffer()constructor — use theBuffer.from/Buffer.allocfactories. - Remember that
.lengthcounts bytes, not characters; useBuffer.byteLength()to size buffers for multi-byte text. - Always specify an encoding when converting to or from strings; UTF-8 is the default but being explicit prevents surprises with
hex/base64. - Validate untrusted sizes before allocating to avoid denial-of-service from oversized requests (
Buffer.allocof a huge size still costs memory). - Treat buffers as fixed-length; to “grow” data, allocate a new buffer and copy, or use
Buffer.concat.