Articles

Node.js Buffer Explained: Binary Data Handling

Node's Buffer class holds raw binary data outside the V8 heap, letting Node handle files, sockets, and streams efficiently. Here's how it works.

Takina Takina · · 4 min read
Dark-themed code editor showing JavaScript

A Buffer is a Node.js class for working with raw binary data — fixed-length sequences of bytes allocated outside the V8 JavaScript heap. Before typed arrays existed in the JavaScript language itself, Node needed a way to represent things like file contents, TCP packets, and image data that don’t fit naturally into JavaScript’s original string and number types, and Buffer was the answer. It’s still the backbone of how Node handles I/O today.

Why JavaScript needed a separate binary type

JavaScript strings are sequences of UTF-16 code units — great for text, awkward for arbitrary binary data like a JPEG’s byte stream or a raw network packet, where not every byte maps cleanly to a valid character. Early Node needed an efficient way to read a file, hold its bytes in memory, and pass them to a socket without going through lossy string conversions or encoding overhead on every operation.

Buffer fills that gap: it’s a fixed-length array of bytes, each one an integer from 0 to 255, allocated directly rather than sitting inside JavaScript’s garbage-collected object heap. That matters for performance — allocating and copying large binary payloads on the regular heap would add unnecessary garbage-collection pressure for exactly the kind of workload Node is often used for.

Creating and inspecting buffers

The common ways to create a buffer:

Buffer.from("hello");          // from a string, UTF-8 by default
Buffer.from([104, 105]);       // from an array of byte values
Buffer.alloc(16);               // 16 zero-filled bytes
Buffer.allocUnsafe(16);          // 16 bytes, uninitialized (faster, but may contain old memory)

Buffer.alloc zeroes its memory before handing it back, which is the safe default. Buffer.allocUnsafe skips that step for speed, but the returned buffer may contain leftover data from previous allocations until you overwrite it — appropriate only when you’re about to fill every byte yourself.

Buffers support array-style indexing and standard methods for reading and writing multi-byte values:

const buf = Buffer.from("Node");
buf[0];                    // 78 — the byte value of "N"
buf.toString("utf-8");     // "Node"
buf.writeUInt32BE(1, 0);   // write a 32-bit integer, big-endian

That writeUInt32BE call matters more than it looks — see big-endian vs little-endian for why byte order is something binary protocols have to agree on explicitly.

Buffer and the wider typed-array family

Modern JavaScript has its own binary data types — ArrayBuffer and typed array views like Uint8Array — that work the same way in the browser and in Node. Buffer actually is a subclass of Uint8Array, extended with Node-specific convenience methods for things like encoding conversion and byte-order reads. If you’re coming from browser code, see JavaScript typed arrays and ArrayBuffer for the platform-neutral version of the same concept — most of what you know there carries over directly to Buffer.

Because Buffer predates the standardized typed-array APIs in the JavaScript language, Node kept it around rather than replacing it outright; new code can use either, and interoperate between them, depending on whether portability to the browser matters.

Where buffers show up in Node’s I/O model

Buffers are the default currency of Node’s streaming APIs. When you read a file or receive data on a socket without specifying a text encoding, you get Buffer chunks, not strings:

const fs = require("node:fs");
fs.readFile("photo.jpg", (err, data) => {
  console.log(Buffer.isBuffer(data)); // true
});

This is deliberate — see Node.js streams explained for how data moves through Node in bounded chunks rather than being loaded entirely into memory. Working with buffers instead of strings avoids repeated encoding and decoding on every chunk, which adds up for large files or high-throughput sockets.

Buffers are also shared safely across worker threads via SharedArrayBuffer-backed views when you need true parallel access to the same binary data rather than copying it between threads — related to the lower-level Atomics and SharedArrayBuffer primitives that make concurrent access to shared memory safe.

Common pitfalls

  • Encoding mismatches. buf.toString() defaults to UTF-8; calling it on binary data that isn’t valid UTF-8 (like a compressed file) will produce garbled or lossy output. Only call toString when you know the bytes represent text.
  • allocUnsafe misuse. Forgetting to fully overwrite an unsafe-allocated buffer before reading from it can leak old memory contents into your output — a real security concern if that buffer is ever sent over the network.
  • Treating buffer length as character length. Buffer.byteLength counts bytes, not characters; multi-byte UTF-8 characters make these diverge, which trips up code that assumes one byte per character.

The takeaway

Buffer is Node’s binary data workhorse: a fixed-length, off-heap byte array that predates JavaScript’s own typed arrays and now extends Uint8Array directly. It’s what file reads, socket data, and stream chunks are made of by default. Use Buffer.alloc unless you have a specific performance reason to reach for allocUnsafe, be deliberate about encoding when converting to and from strings, and treat buffer length as bytes, not characters.

Takina Takina · · 4 min read

npm and pnpm Workspaces: Managing a Monorepo

Workspaces let npm and pnpm manage multiple packages in one repository, sharing a single dependency tree and letting packages reference each other locally.

#JavaScript #Web Development #Developer Tools
Takina Takina · · 4 min read

What Is Biome? ESLint and Prettier in One Rust Tool

Biome is a Rust-based linter and formatter that replaces ESLint and Prettier with one faster tool, one config file, and no plugin ecosystem to manage.

#JavaScript #TypeScript #Developer Tools