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
| Encoding | Use Case |
|---|---|
utf8 | Standard text encoding — the default |
ascii | Basic English text only, one byte per character |
base64 | Encoding binary data as printable text — used for embedding binary data in JSON, URLs, or emails |
hex | Representing each byte as two hexadecimal digits — common for hashes and checksums |
latin1 | Single-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
cryptomodule operates on buffers for hashing, encryption, and generating random bytes - Network protocols — raw TCP data, WebSocket binary frames
- Compression —
zliboperates 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
| Task | Method |
|---|---|
| Create a buffer from a string | Buffer.from(string, encoding) |
| Allocate an empty buffer | Buffer.alloc(size) |
| Allocate without zeroing (faster, unsafe) | Buffer.allocUnsafe(size) |
| Convert a buffer to a string | buffer.toString(encoding) |
| Get the byte length | buffer.length |
| Access or modify a byte | buffer[index] |
| Extract part of a buffer | buffer.subarray(start, end) |
| Join multiple buffers | Buffer.concat([buf1, buf2, ...]) |
| Compare buffer contents | buffer1.equals(buffer2) |