Skip to content
Node.js nd buffers 4 min read

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.

Buffer is a global in Node.js — you don’t need to import it. You can import it explicitly with import { 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 allocUnsafe when 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, use Buffer.alloc.

Which constructor to use

MethodZero-filled?SpeedUse when
Buffer.from(data)N/A (copies/wraps data)FastYou already have bytes, a string, or an array
Buffer.alloc(size)YesSlowerYou need a blank, safe buffer of a fixed size
Buffer.allocUnsafe(size)NoFastestHot 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.alloc for new buffers; reach for allocUnsafe only after measuring and only when you overwrite every byte.
  • Never use the deprecated new Buffer() constructor — use the Buffer.from/Buffer.alloc factories.
  • Remember that .length counts bytes, not characters; use Buffer.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.alloc of a huge size still costs memory).
  • Treat buffers as fixed-length; to “grow” data, allocate a new buffer and copy, or use Buffer.concat.
Last updated June 14, 2026
Was this helpful?