#Buffers
JavaScript strings are for text. But almost everything Node touches at the edges — files, TCP sockets, HTTP bodies, images, gzip data, hashes, encryption keys — is raw bytes, and bytes aren't text until you decide how to decode them. A Buffer is Node's type for a fixed-length sequence of bytes. It's a subclass of Uint8Array, so each element is an integer from 0 to 255, and its memory lives outside the normal JavaScript object heap (it shows up as arrayBuffers in process.memoryUsage()).
A good mental model is a row of numbered boxes, each holding one byte. The row's length is fixed when it's created. You can read and write boxes by index, view part of the row without copying it, and decode the whole row as text using some encoding (utf8, base64, hex, …). Encoding turns text into bytes; decoding turns bytes back into text.
const buffer = Buffer.from("Hello");
console.log(buffer); // <Buffer 48 65 6c 6c 6f>
console.log(buffer.length, buffer[0], buffer.toString("utf-8")); // 5 72 Hello
console.log(buffer instanceof Uint8Array, Buffer.isBuffer(buffer)); // true true
console.log(Buffer.from([72, 105]).toString()); // Hi
console.log(Buffer.from("48656c6c6f", "hex").toString()); // HelloBuffer.from("Hello")encodes the string as UTF-8 (the default) into new memory: five bytes, one per letter, because these are all ASCII characters.console.logshows the bytes in hexadecimal:48is 72 in decimal, the code forH;65ise;6cisl;6fiso.buffer.lengthis the byte count (5), andbuffer[0]reads one byte as a number (72).toString("utf-8")decodes the bytes back into the stringHello("utf-8"and"utf8"are the same).- It really is a
Uint8Array, so typed-array methods (indexOf,subarray,fill, iteration) work on it, and Web APIs that take aUint8Arrayaccept it. Buffer.fromalso accepts an array of byte values or an encoded string:[72, 105]are the bytes forHi, and the hex string"48656c6c6f"decodes to the same five bytes as"Hello".
alloc vs allocUnsafeThere are three ways to get a fresh buffer of a given size, and the difference is about initialisation:
const zeros = Buffer.alloc(8); // zero-filled
console.log(zeros); // <Buffer 00 00 00 00 00 00 00 00>
const unsafe = Buffer.allocUnsafe(8); // NOT initialised — may contain old memory
unsafe.fill(0); // overwrite before you read or send it
const b = Buffer.alloc(4);
b[0] = 255;
b[1] = 256; // too big: stored modulo 256
b[2] = 300;
b[3] = -1;
console.log(b); // <Buffer ff 00 2c ff>
b.writeUInt16BE(0x1234, 0); // two bytes, big-endian, at offset 0
console.log(b, b.readUInt16BE(0), b.readUInt16LE(0)); // <Buffer 12 34 2c ff> 4660 13330Buffer.alloc(8)asks for 8 bytes and zero-fills them. It's always safe, at the cost of writing those zeros.Buffer.allocUnsafe(8)skips the zero-filling. The memory may still hold bytes from whatever used it before — possibly another request's data. It's faster for large buffers you're about to overwrite completely (for example, reading a file into it); otherwisefillit or usealloc.- Each element is an unsigned byte, so assignments are reduced modulo 256:
256 → 0,300 → 44(0x2c),-1 → 255. No error is thrown — an easy way to corrupt data silently. - Multi-byte numbers use the
read…/write…methods.writeUInt16BE(0x1234, 0)stores12 34— big-endian, most significant byte first, the order network protocols use. Reading the same two bytes as little-endian gives0x3412= 13330 instead of 4660: binary formats always specify byte order, and getting it wrong gives plausible-looking garbage.
subarraySlicing a buffer doesn't copy it — and that surprises people who expect array behaviour:
const whole = Buffer.from("Hello, world");
const view = whole.subarray(0, 5); // a window onto the same memory
view[0] = 0x4a; // 'J'
console.log(whole.toString(), "|", view.toString()); // Jello, world | Jello
const copy = Buffer.from(whole.subarray(7)); // Buffer.from(buffer) copies
copy[0] = 0x57; // 'W'
console.log(whole.toString(), "|", copy.toString()); // Jello, world | World
console.log(Buffer.concat([Buffer.from("ab"), Buffer.from("cd")]).toString()); // abcd
console.log(Buffer.from("abc").equals(Buffer.from("abc")), Buffer.from("abc") === Buffer.from("abc")); // true falsesubarray(0, 5)returns a newBufferobject pointing at bytes 0–4 of the same memory. Writing0x4athroughviewchangeswholetoo:HellobecomesJello.- That's why
subarrayis cheap — no bytes move — and it's how parsers pick apart a packet without allocating. The cost: a small view keeps the whole underlying memory alive, so holding a 10-byte view of a 10 MB buffer keeps 10 MB in memory. Buffer.from(existingBuffer)copies the bytes, so changingcopyleaveswholealone.Buffer.concatallocates one new buffer and copies all the pieces into it — the standard way to assemble chunks.===compares object identity, so two buffers with the same bytes are not===. Use.equals()(orBuffer.compare) to compare contents; for secrets such as HMACs, usecrypto.timingSafeEqual.buf.slice()behaves likesubarray(it shares memory too), unlikeArray.prototype.sliceandUint8Array.prototype.slice, which copy. That mismatch is why the docs steer you tosubarray.
allocUnsafe memory