Last updated: 2026-09-14

Buffers

A buffer is a fixed-size chunk of memory used to hold raw binary data. Before Node.js had streams, before it had any of the higher-level abstractions covered so far, it needed a way to represent data that is not text — image bytes, file contents, network packets, encrypted data. The Buffer class is that representation, and it has existed in Node.js since its earliest versions.

JavaScript in the browser was never designed to handle binary data directly — strings and numbers were enough for most of what browsers needed to do. Node.js, working with files and network connections, needed something more fundamental, and Buffer was built to fill that gap.


Why Buffers Exist

A JavaScript string is a sequence of characters, and JavaScript strings are encoded internally as UTF-16. This works well for text, but it is the wrong representation for raw binary data — an image file, for example, is not text at all. It is a sequence of bytes that, if interpreted as UTF-16 text, would be meaningless or would actively corrupt the data.

A Buffer represents raw bytes directly, with no assumption about what those bytes mean. It is Node.js's way of working with binary data safely and efficiently, outside of JavaScript's text-oriented string type.

This connects directly to streams: every chunk that flows through a stream — whether reading a file or receiving an HTTP request body — arrives as a Buffer, unless you explicitly request a string encoding.


Creating a Buffer

From a String

const buf = Buffer.from('Hello, Wariz');
console.log(buf);
// <Buffer 48 65 6c 6c 6f 2c 20 57 61 72 69 7a>

Each pair of hexadecimal digits represents one byte. 48 is the byte value for the letter H, 65 for e, and so on, following the UTF-8 encoding by default.

From an Array of Byte Values

const buf = Buffer.from([72, 101, 108, 108, 111]);
console.log(buf.toString()); // Hello

Allocating an Empty Buffer of a Given Size

const buf = Buffer.alloc(10);
console.log(buf);
// <Buffer 00 00 00 00 00 00 00 00 00 00> — filled with zeros

Buffer.alloc(size) creates a buffer of the specified size in bytes, initialised to zero. This is useful when you know how much space you need but will fill it incrementally.

Buffer.allocUnsafe(size) also creates a buffer of a given size, but does not initialise its contents — it may contain old, leftover data from memory. It is faster than alloc() because it skips the zeroing step, but it should only be used when you are certain the entire buffer will be overwritten before being read, since otherwise it can expose unrelated data that happened to occupy that memory previously.


Converting Between Buffers and Strings

Buffer to String

const buf = Buffer.from('Hello, Wariz');
console.log(buf.toString());        // Hello, Wariz — defaults to utf8
console.log(buf.toString('hex'));   // 48656c6c6f2c20576172697a
console.log(buf.toString('base64')); // SGVsbG8sIFdhcml6

String to Buffer

const buf = Buffer.from('Hello, Wariz', 'utf8');

Common Encodings

EncodingUse Case
utf8Standard text encoding — the default
asciiBasic English text only, one byte per character
base64Encoding binary data as printable text — used for embedding binary data in JSON, URLs, or emails
hexRepresenting each byte as two hexadecimal digits — common for hashes and checksums
latin1Single-byte encoding covering Western European characters

Buffer Operations

Getting the Length in Bytes

const buf = Buffer.from('Hello');
console.log(buf.length); // 5

Note that buf.length is the number of bytes, not necessarily the number of characters — for text using multi-byte UTF-8 characters (emoji, many non-Latin scripts), the byte length and the character count can differ:

const buf = Buffer.from('café');
console.log(buf.length);        // 5 — 'é' takes 2 bytes in UTF-8
console.log('café'.length);     // 4 — 4 characters

Accessing Individual Bytes

A buffer can be indexed directly, like an array, to read or modify individual byte values:

const buf = Buffer.from('Hello');
console.log(buf[0]); // 72 — the byte value for 'H'

buf[0] = 74; // change it to the byte value for 'J'
console.log(buf.toString()); // Jello

Slicing a Buffer

const buf = Buffer.from('Hello, Wariz');
const slice = buf.subarray(0, 5);
console.log(slice.toString()); // Hello

subarray() returns a new Buffer that is a view into a portion of the original — useful when only part of a larger chunk of binary data is needed.

Concatenating Buffers

const buf1 = Buffer.from('Hello, ');
const buf2 = Buffer.from('Wariz');
const combined = Buffer.concat([buf1, buf2]);

console.log(combined.toString()); // Hello, Wariz

Buffer.concat() is the correct way to join multiple buffers — this comes up naturally when collecting chunks from a stream, where each chunk arrives as a separate buffer that eventually needs to be combined into one.

Comparing Buffers

const buf1 = Buffer.from('abc');
const buf2 = Buffer.from('abc');

console.log(buf1.equals(buf2)); // true
console.log(buf1 === buf2);     // false — different objects in memory

Buffer.equals() compares the actual byte content of two buffers, while the strict equality operator === only compares object identity, which is rarely what you actually want when comparing binary data.


Buffers and Streams — Collecting Chunks Manually

When working with a stream directly (without piping it to a destination), the chunks that arrive in 'data' events are buffers by default, unless an encoding is specified. Collecting them into a single complete buffer is a common pattern:

const fs = require('fs');

const readStream = fs.createReadStream('image.png');
const chunks = [];

readStream.on('data', (chunk) => {
  chunks.push(chunk);
});

readStream.on('end', () => {
  const completeBuffer = Buffer.concat(chunks);
  console.log('Total size:', completeBuffer.length, 'bytes');
});

This is the same pattern that was shown for collecting an HTTP request body in the http module topic — the only difference there was converting each chunk with chunk.toString() immediately, which works for text bodies but would corrupt binary data such as an uploaded image. For binary uploads, collecting raw buffers and concatenating them with Buffer.concat() is the correct approach.


A Practical Example — Reading an Image File

const fs = require('fs/promises');

async function getImageInfo(filepath) {
  const buffer = await fs.readFile(filepath);

  console.log('File size:', buffer.length, 'bytes');
  console.log('First 4 bytes (hex):', buffer.subarray(0, 4).toString('hex'));
}

getImageInfo('logo.png');

The first few bytes of a file often identify its type — a PNG file always begins with the bytes 89 50 4e 47. This is how many file-type detection libraries work: reading the first few bytes of a buffer and checking them against known file signatures, rather than trusting a file's extension, which can be incorrect or missing entirely.


Buffers in Practice — Where You Actually Encounter Them

In most application-level code, you rarely construct buffers manually. They surface in specific situations:

  • Reading binary files — images, PDFs, executables — without specifying a text encoding
  • Handling file uploads in a server, where the uploaded content needs to be collected and written to disk or cloud storage
  • Working with cryptography — the crypto module operates on buffers for hashing, encryption, and generating random bytes
  • Network protocols — raw TCP data, WebSocket binary frames
  • Compression — zlib operates on buffers when compressing or decompressing data

For text-based work — reading config files, parsing JSON, handling form data — specifying 'utf8' as the encoding converts everything to strings automatically, and buffers stay invisible. Buffers become relevant the moment binary data enters the picture.


Summary

TaskMethod
Create a buffer from a stringBuffer.from(string, encoding)
Allocate an empty bufferBuffer.alloc(size)
Allocate without zeroing (faster, unsafe)Buffer.allocUnsafe(size)
Convert a buffer to a stringbuffer.toString(encoding)
Get the byte lengthbuffer.length
Access or modify a bytebuffer[index]
Extract part of a bufferbuffer.subarray(start, end)
Join multiple buffersBuffer.concat([buf1, buf2, ...])
Compare buffer contentsbuffer1.equals(buffer2)