Buffer #
A Buffer is a representation of raw binary data in memory — a sequence of bytes that has no interpretation until you give it context. When working with image files, network data, cryptography, binary protocols, or non-standard character encodings, you’re working with Buffers. In the browser JavaScript there’s ArrayBuffer and TypedArray; in Node.js there’s Buffer, which is a subclass of Uint8Array — compatible with the Web API but with very practical additional methods for everyday operations. Understanding Buffer isn’t just about its API, but also about understanding character encodings and how data is represented as bytes.
Creating Buffers #
There are several ways to create a Buffer, each for a different use case.
// Buffer.from() — the most common way, from various sources
// from a string with encoding (default: utf-8)
const buf1 = Buffer.from("Hello, World!", "utf-8");
console.log(buf1); // <Buffer 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21>
console.log(buf1.length); // 13
// from a hex string
const buf2 = Buffer.from("48656c6c6f", "hex");
console.log(buf2.toString("utf-8")); // "Hello"
// from a base64 string
const buf3 = Buffer.from("SGVsbG8sIFdvcmxkIQ==", "base64");
console.log(buf3.toString("utf-8")); // "Hello, World!"
// from an array of bytes
const buf4 = Buffer.from([72, 101, 108, 108, 111]); // ASCII: H, e, l, l, o
console.log(buf4.toString()); // "Hello"
// from another Buffer — creates a COPY, not a reference
const buf5 = Buffer.from(buf1);
buf5[0] = 0x68; // change 'H' to 'h'
console.log(buf1.toString()); // "Hello, World!" — unchanged
console.log(buf5.toString()); // "hello, World!" — only the copy changed
// from an ArrayBuffer (Web API) — shares the same memory!
const arrayBuf = new ArrayBuffer(4);
const buf6 = Buffer.from(arrayBuf);
Buffer.alloc vs Buffer.allocUnsafe #
// Buffer.alloc() — allocate a new zero-filled Buffer (safe)
const bufAman = Buffer.alloc(10);
console.log(bufAman); // <Buffer 00 00 00 00 00 00 00 00 00 00>
// Buffer.alloc() with an initial value
const bufTerisi = Buffer.alloc(5, 0xff);
console.log(bufTerisi); // <Buffer ff ff ff ff ff>
// Buffer.alloc() with a string as the fill
const bufString = Buffer.alloc(10, "ab", "utf-8");
console.log(bufString.toString()); // "ababababab"
// ANTI-PATTERN: Buffer.allocUnsafe() without immediately filling the values
const bufTidakAman = Buffer.allocUnsafe(10);
// ✗ bufTidakAman may contain old data from memory that hasn't been zeroed
// DON'T read its contents before filling it completely
// CORRECT: allocUnsafe only if you IMMEDIATELY fill the entire buffer
const bufUnsafeBenar = Buffer.allocUnsafe(10);
bufUnsafeBenar.fill(0); // or immediately write data across the whole buffer
// ✓ now safe to read
// allocUnsafe is faster than alloc because it doesn't zero-fill
// use only when performance is critical and you're sure you'll fill the whole buffer
Buffer.allocUnsafe()is faster thanBuffer.alloc()because it doesn’t zero-fill, but this means the buffer may contain old data from memory previously used by other processes — including sensitive data like passwords or tokens. Always useBuffer.alloc()unless you have a clear performance reason and are sure you’ll fill the entire buffer before reading it.
Encoding and Decoding #
Encoding determines how bytes are interpreted as text, or how text is converted to bytes. Node.js supports several built-in encodings.
flowchart LR
A["String\n'Hello'"] -- "Buffer.from(str, enc)" --> B["Buffer\n48 65 6c 6c 6f"]
B -- "buf.toString(enc)" --> A
C["Hex String\n'48656c6c6f'"] -- "Buffer.from(hex, 'hex')" --> B
B -- "buf.toString('hex')" --> C
D["Base64\n'SGVsbG8='"] -- "Buffer.from(b64, 'base64')" --> B
B -- "buf.toString('base64')" --> DSupported Encodings #
const teks = "Halo, Dunia! 🌏";
// utf-8 (default) — the standard encoding for Unicode text
const bufUtf8 = Buffer.from(teks, "utf-8");
console.log(bufUtf8.length); // 19 — the emoji takes 4 bytes
// utf-16le — used by Windows and some Microsoft file formats
const bufUtf16 = Buffer.from(teks, "utf-16le");
console.log(bufUtf16.length); // 30 — every character takes at least 2 bytes
// ascii — 7-bit ASCII only, characters outside the range are truncated
const bufAscii = Buffer.from("Hello", "ascii");
console.log(bufAscii.length); // 5
// latin1 / binary — one byte per character (ISO-8859-1)
const bufLatin1 = Buffer.from("café", "latin1");
console.log(bufLatin1.length); // 4
// hex — hexadecimal representation
const bufHex = Buffer.from("deadbeef", "hex");
console.log(bufHex.length); // 4 — every two hex characters = 1 byte
// base64 — the standard encoding for binary data in text
const bufBase64 = Buffer.from("SGVsbG8=", "base64");
console.log(bufBase64.toString()); // "Hello"
// base64url — base64 without +, /, = (URL-safe)
const bufB64url = Buffer.from("SGVsbG8", "base64url");
console.log(bufB64url.toString()); // "Hello"
Converting Between Encodings #
// conversion pattern: string → Buffer → string (different encoding)
function konversiEncoding(
input: string,
dariEncoding: BufferEncoding,
keEncoding: BufferEncoding
): string {
return Buffer.from(input, dariEncoding).toString(keEncoding);
}
// hex to base64
const hex = "48656c6c6f2c20576f726c6421";
const base64 = konversiEncoding(hex, "hex", "base64");
console.log(base64); // "SGVsbG8sIFdvcmxkIQ=="
// base64 to hex
const base64Input = "SGVsbG8sIFdvcmxkIQ==";
const hexOutput = konversiEncoding(base64Input, "base64", "hex");
console.log(hexOutput); // "48656c6c6f2c20576f726c6421"
// utf-8 string to base64 — useful for Basic Auth headers
function encodeBase64(str: string): string {
return Buffer.from(str, "utf-8").toString("base64");
}
function decodeBase64(b64: string): string {
return Buffer.from(b64, "base64").toString("utf-8");
}
// Basic Auth header
const credentials = encodeBase64("username:password");
console.log(`Authorization: Basic ${credentials}`);
// "Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ="
// base64url for JWTs or URL-safe tokens
function encodeBase64URL(data: Buffer): string {
return data.toString("base64url");
}
function decodeBase64URL(b64url: string): Buffer {
return Buffer.from(b64url, "base64url");
}
Reading and Writing Numeric Data #
Buffer provides methods for reading and writing numeric data types in the exact byte format — important when working with binary protocols or structured file formats.
Endianness #
Before reading/writing multi-byte numbers, you need to know endianness — the byte storage order.
Big-Endian (BE): the most significant byte is stored at the smallest address
The number 0x12345678 → [0x12, 0x34, 0x56, 0x78]
Little-Endian (LE): the least significant byte is stored at the smallest address
The number 0x12345678 → [0x78, 0x56, 0x34, 0x12]
Network byte order = Big-Endian (used by TCP/IP network protocols)
x86/x64 CPU = Little-Endian (used by most modern computers)
const buf = Buffer.alloc(8);
// write a 32-bit integer
buf.writeUInt32BE(0x12345678, 0); // Big-Endian at offset 0
console.log(buf.subarray(0, 4)); // <Buffer 12 34 56 78>
buf.writeUInt32LE(0x12345678, 4); // Little-Endian at offset 4
console.log(buf.subarray(4, 8)); // <Buffer 78 56 34 12>
// read a 32-bit integer
console.log(buf.readUInt32BE(0).toString(16)); // "12345678"
console.log(buf.readUInt32LE(4).toString(16)); // "12345678"
// all available read/write methods:
// readUInt8, readInt8
// readUInt16BE, readUInt16LE, readInt16BE, readInt16LE
// readUInt32BE, readUInt32LE, readInt32BE, readInt32LE
// readBigUInt64BE, readBigUInt64LE, readBigInt64BE, readBigInt64LE
// readFloatBE, readFloatLE
// readDoubleBE, readDoubleLE
// example: reading a simple binary protocol header
interface HeaderPaket {
magic: number; // 4 bytes — magic number identifying the protocol
versi: number; // 1 byte — protocol version
tipe: number; // 1 byte — message type
panjangPayload: number; // 4 bytes — payload length in bytes
}
function bacaHeader(buf: Buffer): HeaderPaket {
return {
magic: buf.readUInt32BE(0),
versi: buf.readUInt8(4),
tipe: buf.readUInt8(5),
panjangPayload: buf.readUInt32BE(6),
};
}
function tulisHeader(header: HeaderPaket): Buffer {
const buf = Buffer.alloc(10); // 10 header bytes in total
buf.writeUInt32BE(header.magic, 0);
buf.writeUInt8(header.versi, 4);
buf.writeUInt8(header.tipe, 5);
buf.writeUInt32BE(header.panjangPayload, 6);
return buf;
}
// build a complete packet: header + payload
function buatPaket(tipe: number, payload: Buffer): Buffer {
const header = tulisHeader({
magic: 0xCAFEBABE, // custom magic number
versi: 1,
tipe,
panjangPayload: payload.length,
});
return Buffer.concat([header, payload]);
}
Buffer Operations #
Slice and Subarray #
const buf = Buffer.from("Hello, World!", "utf-8");
// subarray — a view into the same Buffer (shares memory!)
const sub = buf.subarray(7, 12);
console.log(sub.toString()); // "World"
// IMPORTANT: a subarray shares memory with the original buffer
sub[0] = 0x77; // change 'W' to 'w'
console.log(buf.toString()); // "Hello, world!" — the original buffer changed too!
// CORRECT: use Buffer.from() to create an independent copy
const salinan = Buffer.from(buf.subarray(7, 12));
salinan[0] = 0x57; // change back to 'W'
console.log(buf.toString()); // "Hello, world!" — the original buffer is unchanged
Concat — Combining Buffers #
const buf1 = Buffer.from("Hello, ");
const buf2 = Buffer.from("World");
const buf3 = Buffer.from("!");
// ANTI-PATTERN: combining Buffers with + like strings
// Buffer + Buffer doesn't work as expected
// CORRECT: Buffer.concat()
const gabungan = Buffer.concat([buf1, buf2, buf3]);
console.log(gabungan.toString()); // "Hello, World!"
// with a known total length — more efficient
const totalPanjang = buf1.length + buf2.length + buf3.length;
const gabunganEfisien = Buffer.concat([buf1, buf2, buf3], totalPanjang);
// combine chunks from a stream
async function kumpulkanStream(stream: NodeJS.ReadableStream): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks);
}
Copy and Fill #
const sumber = Buffer.from("Hello, World!");
const tujuan = Buffer.alloc(5);
// copy — copy bytes from one buffer to another
sumber.copy(
tujuan, // target buffer
0, // offset in the target (start writing here)
7, // start reading offset from the source
12 // end reading offset from the source (exclusive)
);
console.log(tujuan.toString()); // "World"
// fill — fill the buffer with a certain value
const bufFill = Buffer.alloc(10);
bufFill.fill(0xAA);
console.log(bufFill); // <Buffer aa aa aa aa aa aa aa aa aa aa>
bufFill.fill(0, 5); // fill from offset 5 with zeros
console.log(bufFill); // <Buffer aa aa aa aa aa 00 00 00 00 00>
bufFill.fill("ab", 0, 4, "utf-8"); // fill with a string
console.log(bufFill.toString("utf-8", 0, 4)); // "abab"
Comparing Buffers #
const a = Buffer.from("abc");
const b = Buffer.from("abc");
const c = Buffer.from("abd");
// equals — check whether two buffers are identical
console.log(a.equals(b)); // true
console.log(a.equals(c)); // false
// compare — compare lexicographically
// returns 0 (equal), 1 (a > b), -1 (a < b)
console.log(a.compare(b)); // 0
console.log(a.compare(c)); // -1 (because 'c' < 'd')
console.log(c.compare(a)); // 1
// sort an array of Buffers
const buffers = [
Buffer.from("banana"),
Buffer.from("apple"),
Buffer.from("cherry"),
];
buffers.sort(Buffer.compare);
console.log(buffers.map((b) => b.toString()));
// ["apple", "banana", "cherry"]
// indexOf — find the position of a byte or sub-buffer
const haystack = Buffer.from("Hello, World! Hello!");
const needle = Buffer.from("Hello");
console.log(haystack.indexOf(needle)); // 0
console.log(haystack.indexOf(needle, 1)); // 14 — start searching from offset 1
// includes — check whether the buffer contains a byte or sub-buffer
console.log(haystack.includes("World")); // true
console.log(haystack.includes(0x21)); // true (! = 0x21)
Buffers and Streams #
Buffers and streams are two sides of the same coin — streams flow data in the form of Buffer chunks.
import { Writable, Readable, Transform } from "stream";
// collect an entire stream into a single Buffer
async function streamKeBuffer(readable: NodeJS.ReadableStream): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const chunk of readable) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
}
return Buffer.concat(chunks);
}
// turn a Buffer into a Readable stream (useful for testing or piping)
function bufferKeStream(buf: Buffer): Readable {
const stream = new Readable();
stream.push(buf);
stream.push(null); // end-of-stream signal
return stream;
}
// a Transform stream: process data chunk by chunk
class XORTransform extends Transform {
constructor(private kunci: number) {
super();
}
_transform(chunk: Buffer, _encoding: string, callback: () => void): void {
const hasil = Buffer.alloc(chunk.length);
for (let i = 0; i < chunk.length; i++) {
hasil[i] = chunk[i] ^ this.kunci; // XOR every byte with the key
}
this.push(hasil);
callback();
}
}
// example: simple XOR "encryption" (illustration only, not for production!)
async function xorFile(input: Buffer, kunci: number): Promise<Buffer> {
const chunks: Buffer[] = [];
const transform = new XORTransform(kunci);
transform.on("data", (chunk: Buffer) => chunks.push(chunk));
await new Promise<void>((resolve, reject) => {
transform.on("end", resolve);
transform.on("error", reject);
transform.end(input);
});
return Buffer.concat(chunks);
}
Common Patterns in Real Applications #
Reading Binary Files and Parsing Their Structure #
import { promises as fs } from "fs";
// example: parse a PNG file header
// PNG signature: the first 8 bytes are always 89 50 4E 47 0D 0A 1A 0A
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
interface InfoPNG {
valid: boolean;
lebar: number;
tinggi: number;
bitDepth: number;
colorType: number;
}
async function bacaInfoPNG(filePath: string): Promise<InfoPNG> {
const buf = await fs.readFile(filePath);
// verify the signature
const signature = buf.subarray(0, 8);
if (!signature.equals(PNG_SIGNATURE)) {
return { valid: false, lebar: 0, tinggi: 0, bitDepth: 0, colorType: 0 };
}
// the IHDR chunk starts at offset 8
// IHDR structure: 4-byte length + 4-byte type + 13-byte data + 4-byte CRC
// IHDR data: width (4) + height (4) + bitDepth (1) + colorType (1) + ...
const ihdrOffset = 8 + 4 + 4; // skip the chunk length and type
return {
valid: true,
lebar: buf.readUInt32BE(ihdrOffset),
tinggi: buf.readUInt32BE(ihdrOffset + 4),
bitDepth: buf.readUInt8(ihdrOffset + 8),
colorType: buf.readUInt8(ihdrOffset + 9),
};
}
// example: serialize and deserialize structured data into a compact binary format
interface RecordData {
id: number; // 4-byte uint32
timestamp: bigint; // 8-byte uint64
nilai: number; // 8-byte double
label: string; // 2-byte length + N utf-8 bytes
}
function serializeRecord(record: RecordData): Buffer {
const labelBuf = Buffer.from(record.label, "utf-8");
const totalSize = 4 + 8 + 8 + 2 + labelBuf.length;
const buf = Buffer.alloc(totalSize);
let offset = 0;
buf.writeUInt32BE(record.id, offset); offset += 4;
buf.writeBigUInt64BE(record.timestamp, offset); offset += 8;
buf.writeDoubleBE(record.nilai, offset); offset += 8;
buf.writeUInt16BE(labelBuf.length, offset); offset += 2;
labelBuf.copy(buf, offset);
return buf;
}
function deserializeRecord(buf: Buffer): RecordData {
let offset = 0;
const id = buf.readUInt32BE(offset); offset += 4;
const timestamp = buf.readBigUInt64BE(offset); offset += 8;
const nilai = buf.readDoubleBE(offset); offset += 8;
const labelLength = buf.readUInt16BE(offset); offset += 2;
const label = buf.toString("utf-8", offset, offset + labelLength);
return { id, timestamp, nilai, label };
}
// round-trip test
const record: RecordData = {
id: 42,
timestamp: BigInt(Date.now()),
nilai: 3.14159,
label: "pengukuran-sensor-A",
};
const serialized = serializeRecord(record);
const deserialized = deserializeRecord(serialized);
console.log(deserialized.id); // 42
console.log(deserialized.label); // "pengukuran-sensor-A"
Data Conversion for APIs and the Web #
// encode a file to base64 for sending via a JSON API
async function fileKeBase64(filePath: string): Promise<string> {
const buf = await fs.readFile(filePath);
return buf.toString("base64");
}
// decode base64 from an API and save it as a file
async function base64KeFile(base64: string, outputPath: string): Promise<void> {
const buf = Buffer.from(base64, "base64");
await fs.writeFile(outputPath, buf);
}
// create a data URL from a file (for embedding in HTML/CSS)
async function fileKeDataURL(filePath: string, mimeType: string): Promise<string> {
const buf = await fs.readFile(filePath);
const base64 = buf.toString("base64");
return `data:${mimeType};base64,${base64}`;
}
// example: embed a logo into HTML
const logoDataURL = await fileKeDataURL("logo.png", "image/png");
const html = `<img src="${logoDataURL}" alt="Logo" />`;
// calculate the MD5 hash of a Buffer (for cache busting or ETags)
import { createHash } from "crypto";
function hashBuffer(buf: Buffer, algoritma = "md5"): string {
return createHash(algoritma).update(buf).digest("hex");
}
// generate an ETag for an HTTP response
function generateETag(konten: Buffer | string): string {
const buf = Buffer.isBuffer(konten) ? konten : Buffer.from(konten);
return `"${hashBuffer(buf, "sha256").slice(0, 16)}"`;
}
// efficiently check whether two files are identical
async function fileIdentik(path1: string, path2: string): Promise<boolean> {
const [buf1, buf2] = await Promise.all([
fs.readFile(path1),
fs.readFile(path2),
]);
if (buf1.length !== buf2.length) return false;
return buf1.equals(buf2);
}
Buffer Pooling — Efficient Allocation #
For applications that frequently allocate large numbers of small Buffers, memory pooling can significantly improve performance.
// Node.js already has built-in pooling for Buffer.allocUnsafe() < 4KB
// but for more control, you can build your own pool
class BufferPool {
private pool: Buffer;
private offset: number = 0;
private readonly ukuranPool: number;
constructor(ukuranPool: number = 64 * 1024) { // 64KB by default
this.ukuranPool = ukuranPool;
this.pool = Buffer.allocUnsafe(ukuranPool);
}
// take a slice from the pool — very fast, no new memory allocation
ambil(ukuran: number): Buffer {
if (ukuran > this.ukuranPool) {
// too large for the pool — allocate normally
return Buffer.allocUnsafe(ukuran);
}
if (this.offset + ukuran > this.ukuranPool) {
// pool exhausted — create a new pool
this.pool = Buffer.allocUnsafe(this.ukuranPool);
this.offset = 0;
}
const slice = this.pool.subarray(this.offset, this.offset + ukuran);
this.offset += ukuran;
return slice;
}
}
const pool = new BufferPool();
// allocate small buffers from the pool — more efficient than Buffer.alloc every time
const buf = pool.ambil(128);
buf.fill(0); // always fill first because it comes from allocUnsafe
When to Use Buffers #
Use Buffers for:
✓ Reading and writing binary files — images, PDFs, audio, video
✓ Network data — TCP sockets, custom binary protocols
✓ Cryptography — hash input/output, encryption, digital signatures
✓ Encoding conversion — hex ↔ base64 ↔ utf-8
✓ Parsing structured binary formats — file headers, network protocols
✓ Transferring binary data via JSON using base64
✓ Operations that need direct control over individual bytes
No Buffer needed if:
✗ Only working with text — use plain strings
✗ Data is already in JSON format — no need to convert to a Buffer
✗ Simple string operations — split, replace, match
Summary #
Buffer.from()for creating Buffers from existing sources — strings, hex, base64, arrays of bytes, or other Buffers. Always specify the encoding explicitly to avoid ambiguity.Buffer.alloc()notBuffer.allocUnsafe()for general code —allocdoes safe zero-filling;allocUnsafeis only for performance optimization when you’re sure you’ll fill the entire buffer before reading.subarray()shares memory with the original buffer — modifying the subarray changes the original buffer. UseBuffer.from(buf.subarray(...))to get an independent copy.Buffer.concat()for combining buffers — there’s no+operator for Buffers; always useBuffer.concat([...buffers]).- Specify endianness correctly — use
BE(Big-Endian) for network protocols andLE(Little-Endian) for file formats created on x86/x64; wrong endianness produces wrong numbers without an error.equals()for comparing buffer contents — the===operator only compares references, not contents. For security, usecrypto.timingSafeEqual().Buffer.concat(chunks)after collecting all stream chunks — collect chunks into an array first, then concat once at the end; don’t concat one by one inside a loop because it causes O(n²) memory allocations.- Base64 for transferring binary data via JSON — use
buf.toString('base64')to encode andBuffer.from(str, 'base64')to decode when you need to include binary data in a JSON API.