Buffers & Binary Data

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Node.js 26 (26.10.0, released 2026-09-21, the Current line that becomes LTS on 2026-10-28), with the differences of Node.js 24 "Krypton" (24.21.0, Active LTS) and Node.js 22 "Jod" (22.23.3, Maintenance) called out; versions and stability levels were checked against the official documentation on 2026-10-03, and every example below was run on all three lines unless the text says otherwise. JavaScript strings are sequences of UTF-16 code units; files, sockets, hashes, images and protocol frames are sequences of bytes. Everything that crosses that boundary in Node.js goes through one of a few classes, and this page is the map: Buffer (Stability 2, Stable) is Node.js’s byte container and a subclass of Uint8Array; ArrayBuffer, the typed arrays and DataView are the language-level building blocks Buffer sits on; node:string_decoder (Stability 2, Stable) decodes byte streams without splitting characters; TextEncoder and TextDecoder are the WHATWG Encoding Standard classes, available as globals; and Blob and File are the immutable, shareable byte containers of the web platform. For the JavaScript side of typed arrays read JavaScript Standard Library Utilities; this page owns what Node.js adds. The running scenario is the Bookshelf application that grows across this section, described in Introduction & Architecture; here it parses a binary record format and sniffs cover images. Streams deliver Buffer chunks, so Streams and The File System are the natural next reads.

The stability and version map

Table 1. APIs covered on this page
API Status (Node.js 26 docs) Version notes

Buffer class and the node:buffer module

2 Stable

Buffer is available as a global, but the documentation recommends importing it from node:buffer. Buffer.copyBytesFrom() was added in v19.8.0 and v18.16.0.

Buffer.alloc(), Buffer.allocUnsafe(), Buffer.allocUnsafeSlow()

Stable

An alignment argument was added to allocUnsafe() and allocUnsafeSlow() in v26.8.0.

Buffer.poolSize

Stable

Default raised from 8192 to 65536 in v26.3.0 and v24.18.0, so it is still 8192 on Node.js 22 (measured: 8192 on 22.23.3, 65536 on 24.21.0 and 26.10.0).

new Buffer() and Buffer()

0 Deprecated (DEP0005)

Application deprecation (code outside node_modules only) with a runtime warning since v10.0.0.

buf.slice()

0 Deprecated (DEP0158)

Documentation-only deprecation since v17.5.0 and v16.15.0; use buf.subarray().

buf.parent

0 Deprecated

Use buf.buffer.

SlowBuffer

Removed (DEP0030, End-of-Life)

Documentation-only in v6.0.0, runtime deprecation in v24.0.0, removed in v25.0.0. It is still exported on Node.js 24 and 22.

buffer.atob() and buffer.btoa()

3 Legacy

Use Buffer.from(data, 'base64') and buf.toString('base64').

buffer.isUtf8(), buffer.isAscii(), buffer.transcode()

Stable

isUtf8() and isAscii() were added in v19.4.0 and v19.6.0 (also back-ported to v18); since v26.8.0 both treat detached `ArrayBuffer`s (and views over them) as empty.

Uint8Array.prototype.toBase64(), toHex(), Uint8Array.fromBase64(), fromHex(), setFromBase64(), setFromHex()

ECMAScript feature of V8, not a Node.js API

Not documented by Node.js. Observed: present on Node.js 26.10.0 (V8 14.6), absent on 24.21.0 (V8 13.6) and 22.23.3 (V8 12.4).

node:string_decoder

2 Stable

Each invalid character is replaced by a single replacement character since v8.0.0.

TextEncoder, TextDecoder

Globals (also node:util)

Global since v11.0.0. The supported encodings depend on the ICU configuration of the build.

Blob

Stable

No longer experimental since v18.0.0 and v16.17.0; blob.bytes() was added in v22.3.0 and v20.16.0.

File

Stable

No longer experimental since v20.0.0; instances became cloneable in v23.0.0.

fs.openAsBlob()

Stable

Stable since v24.0.0 and v22.17.0; fs.openAsBlobSync() was added in v26.10.0.

What a Buffer is

A Buffer is a fixed-length sequence of bytes. It cannot grow or shrink; to make one longer you allocate a new one and copy. It is implemented as a subclass of Uint8Array, so every typed-array method works on it and every Node.js API that accepts a Buffer also accepts a plain Uint8Array. The largest allowed size is exposed as buffer.constants.MAX_LENGTH (also buffer.kMaxLength); on 64-bit builds it is Number.MAX_SAFE_INTEGER since v22.0.0, so memory, not the constant, is the real limit. buffer.constants.MAX_STRING_LENGTH is the largest string, counted in UTF-16 code units (536870888 on the Node.js versions used for this page).

The mental model that prevents most bugs is that a Buffer is a view onto an ArrayBuffer`: it has a `buffer (the underlying ArrayBuffer), a byteOffset into it and a length. Some creation methods copy bytes and some share them; the sections below mark which.

Creating buffers: from, alloc, allocUnsafe, concat

Three families of factory methods replaced the old constructor. Buffer.from() copies (or wraps) existing data, Buffer.alloc() returns zero-filled memory, and Buffer.allocUnsafe() returns memory that may contain old bytes and is therefore only for code that overwrites every byte before reading any. Buffer.concat(list[, totalLength]) copies several buffers into a new one and truncates or zero-pads to totalLength when it is given. Buffer.byteLength(string, encoding) tells you how many bytes a string needs without allocating.

Creating, measuring and joining buffers
import { Buffer } from 'node:buffer';

const fromString = Buffer.from('Café ☕');          // UTF-8 by default
const fromArray = Buffer.from([0x42, 0x4f, 0x4f, 0x4b]);
const zeroed = Buffer.alloc(8);                    // always zero-filled
const filled = Buffer.alloc(8, 'ab');              // repeated fill pattern
const scratch = Buffer.allocUnsafe(8).fill(0);     // uninitialised until overwritten

console.log(fromString, fromString.length, fromString.toString().length);
console.log(Buffer.byteLength('Café ☕'), Buffer.byteLength('Café ☕', 'utf16le'));
console.log(fromArray.toString('latin1'));
console.log(zeroed, filled.toString(), scratch);

const parts = [Buffer.from('Boo'), Buffer.from('k'), Buffer.from('shelf')];
const joined = Buffer.concat(parts);               // copies; total length computed
console.log(joined.toString(), Buffer.concat(parts, 4).toString());
console.log(Buffer.isBuffer(joined), Buffer.isBuffer(new Uint8Array(2)), joined instanceof Uint8Array);
Output (identical on Node.js 26, 24 and 22)
<Buffer 43 61 66 c3 a9 20 e2 98 95> 9 6
9 12
BOOK
<Buffer 00 00 00 00 00 00 00 00> abababab <Buffer 00 00 00 00 00 00 00 00>
Bookshelf Book
true false true

The string 'Café ☕' has six characters but nine UTF-8 bytes: length of a Buffer is always bytes, never characters. The reference table below lists the creation API.

Table 2. Buffer creation methods
Method Copies or shares Notes

Buffer.from(array)

Copies

Each value is truncated with value & 255.

Buffer.from(string[, encoding])

Copies

Encoding defaults to 'utf8'.

Buffer.from(buffer)

Copies

Also accepts a Uint8Array.

Buffer.from(arrayBuffer[, byteOffset[, length]])

Shares

Writes through the Buffer are visible in the ArrayBuffer and the reverse.

Buffer.copyBytesFrom(view[, offset[, length]])

Copies

Copies the bytes of a typed array or DataView, whatever its element size.

Buffer.alloc(size[, fill[, encoding]])

New zeroed memory

Slower than allocUnsafe(), safe by default.

Buffer.allocUnsafe(size[, alignment])

New uninitialised memory

May use the shared pool when size is at most half of Buffer.poolSize.

Buffer.allocUnsafeSlow(size[, alignment])

New uninitialised memory

Never uses the shared pool: its own ArrayBuffer.

Buffer.concat(list[, totalLength])

Copies

Pass totalLength when you know it to skip a length pass.

The pool, and why allocUnsafe is "unsafe"

To make small allocations fast, Buffer.allocUnsafe(), Buffer.from(string), Buffer.concat() and Buffer.from(array) may carve a slice out of a shared, pre-allocated block (the pool, Buffer.poolSize bytes) when the requested size is at most half the pool size. Buffer.allocUnsafeSlow() never does. Two consequences matter in practice. First, memory from allocUnsafe() is uninitialised, so reading it before overwriting every byte can leak whatever the process had in that memory earlier, including secrets from another request. Second, a pooled Buffer shares its ArrayBuffer with unrelated buffers, so buf.buffer is bigger than buf and buf.byteOffset is usually not 0; never pass buf.buffer to code that expects exactly your bytes, and keep a small slice of a pooled block alive only if you accept pinning the whole block. A long-lived small buffer is the use case for allocUnsafeSlow().

The command-line option --zero-fill-buffers makes every newly allocated Buffer zero-filled, including the unsafe ones. It exists to enforce safety in code you cannot audit and has a measurable performance cost, so it is a mitigation, not a default. Measured on all three lines:

Forcing zero-filled allocations
node --zero-fill-buffers -p "Buffer.allocUnsafe(5)"
# <Buffer 00 00 00 00 00>

Encodings

Converting between strings and bytes always names an encoding. The default is UTF-8. Names are case-insensitive. The supported encodings are:

Table 3. Buffer encodings
Encoding Kind Notes

'utf8' (alias 'utf-8')

Text

Default. Invalid byte sequences decode to the replacement character U+FFFD.

'utf16le' (aliases 'utf-16le', 'ucs2', 'ucs-2')

Text

Little-endian only; 2 or 4 bytes per character.

'latin1' (alias 'binary')

Text

One byte per character for U+0000 to U+00FF; other characters are truncated into that range. latin1 has been an alias for binary since v6.4.0.

'ascii'

Legacy text

7-bit data. When decoding it clears the high bit of every byte. Prefer 'utf8' or 'latin1'.

'base64'

Binary-to-text

Accepts the URL-safe alphabet and ignores whitespace when decoding.

'base64url'

Binary-to-text

Added in v15.7.0 and v14.18.0. Omits padding when encoding; accepts regular base64 when decoding.

'hex'

Binary-to-text

Two characters per byte. Decoding stops silently at the first invalid pair or at a trailing odd digit.

For the binary-to-text encodings the naming is reversed: turning a Buffer into a base64 string is "encoding" and the reverse is "decoding". Buffer.isEncoding(name) tells you whether a name is supported.

Encodings and their traps
import { Buffer } from 'node:buffer';

const buf = Buffer.from('hello world', 'utf8');
console.log(buf.toString('hex'));                  // 68656c6c6f20776f726c64
console.log(buf.toString('base64'));               // aGVsbG8gd29ybGQ=
console.log(Buffer.from([0xfb, 0xff, 0xfe]).toString('base64'));     // + and / alphabet, padded
console.log(Buffer.from([0xfb, 0xff, 0xfe]).toString('base64url'));  // - and _ alphabet, no padding
console.log(Buffer.from('-__-', 'base64url'), Buffer.from('+//+', 'base64url'));
console.log(Buffer.from('1ag123', 'hex'), Buffer.from('1a7', 'hex'), Buffer.from('1634', 'hex'));
console.log(Buffer.from('fhqw', 'utf16le'), Buffer.from('é', 'latin1'), Buffer.from('€', 'latin1'));
console.log(Buffer.from([0xe2, 0x82]).toString(), Buffer.from([0xff]).toString().codePointAt(0).toString(16));
console.log(Buffer.isEncoding('UTF-8'), Buffer.isEncoding('utf/8'), Buffer.isEncoding('binary'));
Output (identical on Node.js 26, 24 and 22)
68656c6c6f20776f726c64
aGVsbG8gd29ybGQ=
+//+
-__-
<Buffer fb ff fe> <Buffer fb ff fe>
<Buffer 1a> <Buffer 1a> <Buffer 16 34>
<Buffer 66 00 68 00 71 00 77 00> <Buffer e9> <Buffer ac>
� fffd
true false true

Read the output line by line. Malformed hex is truncated, not rejected ('1ag123' gives one byte), so validate hex input before decoding it. 'latin1' silently destroys characters outside U+0000 to U+00FF (the euro sign became the byte ac). A truncated UTF-8 sequence decodes to the replacement character U+FFFD, which is lossy: you cannot get the original bytes back from the string. Never convert binary data to a string with toString() (UTF-8) and back; use 'base64', 'hex' or keep the Buffer.

Slicing: subarray shares, copies copy

buf.subarray([start[, end]]) returns a new Buffer that references the same memory, offset and cropped; negative indexes count from the end and an end past the length is clamped. It is inherited from TypedArray.prototype.subarray(). The older buf.slice() behaves the same on Buffer but is deprecated (DEP0158, documentation-only) because it is incompatible with Uint8Array.prototype.slice(), which copies; the same name meaning "view" on one class and "copy" on its superclass was a bug magnet. To copy, use Buffer.from(buffer), buf.copy(target, …​) or Buffer.concat([buf]).

A view, a copy, and the pool
import { Buffer } from 'node:buffer';

const original = Buffer.from('bookshelf');
const view = original.subarray(0, 4);          // no copy: shares memory
const copy = Buffer.from(original.subarray(4)); // Buffer.from(buffer) copies
view[0] = 0x42;                                 // 'B'
console.log(original.toString(), view.toString(), copy.toString());

copy[0] = 0x53;                                 // does not touch original
console.log(original.toString(), copy.toString());

console.log(original.subarray(-5).toString(), original.subarray(2, 100).toString());

// Small Buffers share one pooled ArrayBuffer, so byteOffset is usually not 0.
const small = Buffer.from('abc');
console.log(small.byteOffset > 0, small.buffer.byteLength > small.length, Buffer.poolSize);
const exact = Buffer.allocUnsafeSlow(3);
console.log(exact.byteOffset, exact.buffer.byteLength);   // 0 3: its own ArrayBuffer
Output (Node.js 26 and 24; Node.js 22 prints 8192 instead of 65536 on the fourth line)
Bookshelf Book shelf
Bookshelf Shelf
shelf okshelf
true true 65536
0 3

The first line shows that writing through view changed original (Bookshelf), while copy stayed independent. The value byteOffset > 0 is the usual case for pooled buffers but not guaranteed (a buffer can start the pool), which is why the example prints a comparison and not an exact offset.

Reading and writing binary structures

Buffer has a method per integer and float width and endianness: readUInt8(), readUInt16LE(), readUInt16BE(), readUInt32LE(), readBigUInt64BE(), readFloatLE(), readDoubleBE() and the matching write…​() methods, plus readUIntBE(offset, byteLength) and readIntLE(offset, byteLength) for 1 to 6 byte values. Every write…​() method returns the offset just past what it wrote, so calls chain naturally; buf.write(string, offset, length, encoding) returns the number of bytes written. Endianness is in the method name, which makes mistakes visible in review: network protocols are usually big-endian (BE), most CPUs you run on are little-endian (LE). Reading outside the buffer throws a RangeError with code ERR_OUT_OF_RANGE instead of returning garbage. The search and comparison helpers are indexOf(), lastIndexOf(), includes() (all accept a number, a string with an encoding, or a Buffer), equals(), compare() and Buffer.compare(), fill(), copy(), swap16(), swap32(), swap64() and toJSON().

The example is a small, original binary header for a Bookshelf catalogue record: a four-byte magic number, a version, a flags byte, a big-endian year, a little-endian price in cents and a fixed 16-byte title.

Writing and parsing a fixed-layout record
import { Buffer } from 'node:buffer';

// A tiny binary record: magic "BKSH", version, flags, year, price in cents, 16-byte title.
const record = Buffer.alloc(4 + 1 + 1 + 2 + 4 + 16);
let offset = 0;
offset += record.write('BKSH', offset, 'ascii');
offset = record.writeUInt8(1, offset);             // write* returns the next offset
offset = record.writeUInt8(0b101, offset);
offset = record.writeUInt16BE(2024, offset);
offset = record.writeUInt32LE(1999, offset);       // endianness is in the method name
record.write('Efficient Node.js', offset, 16, 'utf8');
console.log(record.toString('hex'));

console.log(record.toString('ascii', 0, 4), record.readUInt8(4), record.readUInt16BE(6), record.readUInt32LE(8));
console.log(record.toString('utf8', 12).replace(/\0+$/, ''));
console.log(record.readUInt8(4), record.subarray(0, 4).equals(Buffer.from('BKSH')));
console.log(Buffer.compare(Buffer.from('a'), Buffer.from('b')), record.indexOf('BKSH'), record.includes(0x07));
try { record.readUInt32LE(record.length - 2); } catch (e) { console.log(e.code); }
Output (identical on Node.js 26, 24 and 22)
424b5348010507e8cf070000456666696369656e74204e6f64652e6a
BKSH 1 2024 1999
Efficient Node.j
1 true
-1 0 true
ERR_OUT_OF_RANGE

Notice that the 17-character title was cut to 16 bytes (Efficient Node.j): write() never writes a partial multi-byte character, but it does truncate, so size fixed fields for the longest value you accept, or reject longer values explicitly. For anything beyond a handful of fields, a DataView (below) or a small parser class is easier to keep correct than offsets scattered through the code.

ArrayBuffer, TypedArrays and DataView

ArrayBuffer is a raw, fixed-length block of memory with no way to read it. A typed array (Uint8Array, Int16Array, Uint32Array, Float32Array, BigInt64Array, and so on) is a view that interprets a range of an ArrayBuffer as numbers of one type, using the platform’s byte order (little-endian on every mainstream Node.js platform). A DataView is a view that reads and writes any type at any byte offset with an explicit endianness argument (big-endian by default), which is why it is the right tool for protocol and file formats. SharedArrayBuffer is the variant that can be shared between threads; it is covered in Worker Threads. Buffer fits in as a Uint8Array subclass with extra methods, so the conversions between the worlds are cheap and mostly about whether memory is shared.

Moving between Buffer, ArrayBuffer, typed arrays and DataView
import { Buffer } from 'node:buffer';

const ab = new ArrayBuffer(8);
const bytes = new Uint8Array(ab);
const asBuffer = Buffer.from(ab, 2, 4);            // shares memory with ab, bytes 2..5
asBuffer.fill(0xff);
console.log(bytes);                                // changes visible through the Uint8Array

const f32 = new Float32Array([1.5, -2]);
const copy = Buffer.copyBytesFrom(f32);            // copies the bytes of the view
const alias = Buffer.from(f32.buffer, f32.byteOffset, f32.byteLength); // aliases them
f32[0] = 3;
console.log(copy.readFloatLE(0), alias.readFloatLE(0));

const dv = new DataView(new ArrayBuffer(4));
dv.setUint32(0, 0x01020304);                       // DataView is big-endian by default
console.log(new Uint8Array(dv.buffer), new Uint8Array(new Uint32Array([0x01020304]).buffer));
console.log(Buffer.from('hi').toJSON());
const u8 = new Uint8Array([104, 105]);
console.log(Buffer.from(u8.buffer, u8.byteOffset, u8.length).toString());   // wrap, do not copy
console.log(Object.getPrototypeOf(Buffer.prototype) === Uint8Array.prototype);
Output (identical on Node.js 26, 24 and 22)
Uint8Array(8) [
    0,   0, 255, 255,
  255, 255,   0,   0
]
1.5 3
Uint8Array(4) [ 1, 2, 3, 4 ] Uint8Array(4) [ 4, 3, 2, 1 ]
{ type: 'Buffer', data: [ 104, 105 ] }
hi
true

The output shows the rule in action: asBuffer wrote bytes 2 to 5 of the shared ArrayBuffer; copy kept the old float while alias followed the change (1.5 versus 3); and the same number 0x01020304 is laid out big-endian by DataView and little-endian by Uint32Array on this machine. Buffer.from(typedArray) is not the way to get the bytes of a Float32Array: it copies the element values, each truncated to a byte (observed: Buffer.from(new Float32Array([1.5, 300])) is <Buffer 01 2c>). Use Buffer.copyBytesFrom() (copy) or Buffer.from(view.buffer, view.byteOffset, view.byteLength) (alias).

Base64 and hex on plain Uint8Array

A recent ECMAScript addition gives Uint8Array its own conversions: toBase64(), toHex(), Uint8Array.fromBase64(), Uint8Array.fromHex() and the in-place setFromBase64() and setFromHex(), with options for the 'base64' or 'base64url' alphabet and for omitting padding. These come from V8, not from Node.js, and are not part of the Node.js API documentation, so treat what follows as observed: they exist on Node.js 26.10.0 (V8 14.6) and are missing on 24.21.0 (V8 13.6) and 22.23.3 (V8 12.4). Feature-detect and fall back to Buffer, which runs everywhere and is what Node.js-only code should keep using. The helpers matter for code that must also run in browsers and other runtimes, where Buffer does not exist.

Feature-detecting the native helpers
const bytes = new Uint8Array([0xfb, 0xff, 0xfe, 0x00]);

if (typeof bytes.toBase64 === 'function') {
  console.log(bytes.toBase64());
  console.log(bytes.toBase64({ alphabet: 'base64url', omitPadding: true }));
  console.log(bytes.toHex());
  console.log(Uint8Array.fromBase64('+//+AA=='), Uint8Array.fromHex('68656c6c6f'));
  const target = new Uint8Array(8);
  console.log(target.setFromHex('cafe'), target);
} else {
  console.log('no native Uint8Array base64/hex helpers; use Buffer');
  const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  console.log(buf.toString('base64'), buf.toString('base64url'), buf.toString('hex'));
}
Output on Node.js 26.10.0
+//+AA==
-__-AA
fbfffe00
Uint8Array(4) [ 251, 255, 254, 0 ] Uint8Array(5) [ 104, 101, 108, 108, 111 ]
{ read: 4, written: 2 } Uint8Array(8) [
  202, 254, 0, 0,
    0,   0, 0, 0
]
Output on Node.js 24.21.0 and 22.23.3 (the fallback branch)
no native Uint8Array base64/hex helpers; use Buffer
+//+AA== -__-AA fbfffe00

Unlike Buffer.from(hex, 'hex'), which silently truncates malformed input, the native fromHex() and fromBase64() throw on invalid input (observed on Node.js 26.10.0: Uint8Array.fromHex('1ag1') and Uint8Array.fromBase64('@@') both throw a SyntaxError).

Buffer methods on a plain Uint8Array

The Buffer documentation states that all methods on the Buffer prototype are callable with a Uint8Array instance, which is handy for library code that receives either. This is documented from the Node.js 24 documentation onward and is absent from the 22.x documentation.

Borrowing Buffer methods (Node.js 26 and 24)
const { toString, write } = Buffer.prototype;
const u8 = new Uint8Array(5);

write.call(u8, 'hello', 0, 5, 'utf8');   // Buffer methods applied to a plain Uint8Array
console.log(toString.call(u8, 'utf8'), toString.call(u8, 'hex'));
Output on Node.js 26 and 24
hello 68656c6c6f

Observed on Node.js 22.23.3: the same code throws TypeError: this.utf8Write is not a function, so on the 22 line wrap the view with Buffer.from(u8.buffer, u8.byteOffset, u8.length) first.

SlowBuffer and the deprecated constructor

Two older ways of getting a buffer are worth recognising in existing code. SlowBuffer was a class that returned an unpooled buffer; it was deprecated in documentation in v6.0.0, produced a runtime warning from v24.0.0 and was removed in v25.0.0 (DEP0030, End-of-Life). Its replacement is Buffer.allocUnsafeSlow(size); a codemod, npx codemod@latest @nodejs/slow-buffer-to-buffer-alloc-unsafe-slow, performs the migration. The Buffer() function and new Buffer() constructor (DEP0005) chose its behaviour from the type of its first argument, so an attacker who could make a JSON field a number instead of a string made the server allocate memory of that size; before Node.js 8.0.0 the buffer was also uninitialised, which exposed process memory. It still works, but code outside node_modules prints a deprecation warning. Use Buffer.from(), Buffer.alloc() or Buffer.allocUnsafe() and validate the type of the argument yourself.

SlowBuffer export and the constructor warning
const buffer = require('node:buffer');

console.log('SlowBuffer export:', typeof buffer.SlowBuffer);
if (buffer.SlowBuffer) console.log(new buffer.SlowBuffer(4).length);   // 24/22: warns (DEP0030)
console.log(Buffer.allocUnsafeSlow(4).length);                          // the replacement

const legacy = new Buffer(10);                                          // DEP0005 warning
console.log(legacy.length);
Output (standard output; warnings go to standard error)
# Node.js 26
SlowBuffer export: undefined
4
10
(node:PID) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.

# Node.js 24
SlowBuffer export: function
4
4
10
(node:PID) [DEP0030] DeprecationWarning: SlowBuffer() is deprecated. Please use Buffer.allocUnsafeSlow()
(node:PID) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.

# Node.js 22
SlowBuffer export: function
4
4
10
(node:PID) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.

The warning text was observed on the three lines (the example uses CommonJS because the new Buffer() warning is the point and it is the form legacy code takes). The Node.js 22 line has no runtime warning for SlowBuffer, consistent with the version history above.

StringDecoder: decoding a stream without splitting characters

Bytes arrive in chunks whose boundaries have nothing to do with character boundaries. A UTF-8 character is one to four bytes, so a chunk can end in the middle of one; calling chunk.toString() on each chunk and concatenating produces replacement characters. StringDecoder (node:string_decoder) keeps the incomplete trailing bytes in an internal buffer: write(chunk) returns the longest decodable prefix and holds the rest for the next call, and end([chunk]) flushes whatever remains, replacing an incomplete sequence with a substitution character. It supports 'utf8' (the default), 'utf16le', 'base64', 'base64url', 'hex', 'latin1' and 'ascii' (observed: 'binary' and 'ucs2' are accepted as aliases of 'latin1' and 'utf16le'). A decoder can be reused after end().

Chunk boundaries inside a character
import { StringDecoder } from 'node:string_decoder';
import { Buffer } from 'node:buffer';

const bytes = Buffer.from('Écrire un café ☕ à Paris');
const chunks = [bytes.subarray(0, 1), bytes.subarray(1, 14), bytes.subarray(14, 16), bytes.subarray(16)];

console.log(chunks.map((c) => c.toString()).join(''));      // broken: splits multi-byte characters

const decoder = new StringDecoder('utf8');
let text = '';
for (const chunk of chunks) text += decoder.write(chunk);
text += decoder.end();
console.log(text);

const d2 = new StringDecoder('utf8');
console.log(JSON.stringify(d2.write(Buffer.from([0xe2, 0x82]))), JSON.stringify(d2.end()));
const d3 = new StringDecoder('base64');
console.log(d3.write(Buffer.from('ab')), d3.end());
Output (identical on Node.js 26, 24 and 22)
��crire un café ☕ à Paris
Écrire un café ☕ à Paris
"" "�"
 YWI=

You rarely construct a StringDecoder yourself: a readable stream configured with an encoding (readable.setEncoding('utf8') or the encoding option of fs.createReadStream()) uses one internally. The next example shows the two correct ways to read text in small chunks, and a second Bookshelf task: sniffing a file type from its first bytes without reading the whole file.

Sniffing a PNG signature and collecting chunks correctly
import { Buffer } from 'node:buffer';
import { createReadStream } from 'node:fs';
import { open, writeFile, rm } from 'node:fs/promises';

const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);

// Sniff a cover image by reading only its first bytes into a fixed-size Buffer.
async function isPng(path) {
  const handle = await open(path, 'r');
  try {
    const head = Buffer.alloc(PNG_SIGNATURE.length);
    const { bytesRead } = await handle.read(head, 0, head.length, 0);
    return bytesRead === head.length && head.equals(PNG_SIGNATURE);
  } finally {
    await handle.close();
  }
}

await writeFile('cover.bin', Buffer.concat([PNG_SIGNATURE, Buffer.from('rest of the image')]));
await writeFile('notes.txt', 'Écrire des café ☕ à Paris, 日本語');
console.log(await isPng('cover.bin'), await isPng('notes.txt'));

// Collect a stream: keep chunks as Buffers and decode once at the end.
const chunks = [];
for await (const chunk of createReadStream('notes.txt', { highWaterMark: 5 })) chunks.push(chunk);
console.log(chunks.length, Buffer.concat(chunks).toString());

// Or let the stream decode for you: the encoding option uses a StringDecoder internally.
let text = '';
for await (const piece of createReadStream('notes.txt', { highWaterMark: 5, encoding: 'utf8' })) text += piece;
console.log(text);
await rm('cover.bin');
await rm('notes.txt');
Output (identical on Node.js 26, 24 and 22)
true false
9 Écrire des café ☕ à Paris, 日本語
Écrire des café ☕ à Paris, 日本語

Collecting Buffer chunks and calling Buffer.concat(chunks) once, then decoding, is also the right shape for binary data; for large inputs do not collect at all and process chunks as they arrive, as described in Streams.

TextEncoder and TextDecoder

TextEncoder and TextDecoder implement the WHATWG Encoding Standard, the same classes browsers provide. They are globals, and also exported by node:util. They are the portable choice for code shared with browsers, and the only choice for encodings other than the ones Buffer knows. TextEncoder only produces UTF-8 (its encoding property is always 'utf-8'): encode(string) returns a Uint8Array, and encodeInto(source, destination) writes into an existing array and reports read (UTF-16 code units consumed) and written (bytes produced), stopping before it would split a character. TextDecoder takes an encoding label (default 'utf-8') and options: fatal: true throws a TypeError with code ERR_ENCODING_INVALID_ENCODED_DATA on invalid input instead of inserting replacement characters, and ignoreBOM: true keeps a leading byte order mark in the output instead of stripping it. decode(input, { stream: true }) keeps incomplete trailing bytes for the next call, like StringDecoder. With full ICU data (the default in official builds), TextDecoder supports the WHATWG encoding list, including windows-1252, iso-8859-*, shift_jis, gbk, big5, euc-kr and utf-16le; builds with small-icu or without Intl support support fewer, which util.html tabulates.

The WHATWG codecs
const encoder = new TextEncoder();
const dest = new Uint8Array(6);
console.log(encoder.encode('€uro'), encoder.encodeInto('€uro', dest), dest);

const decoder = new TextDecoder();
console.log(decoder.decode(new Uint8Array([0xe2, 0x82, 0xac])));
console.log(JSON.stringify(decoder.decode(new Uint8Array([0xff]))));
try { new TextDecoder('utf-8', { fatal: true }).decode(new Uint8Array([0xff])); }
catch (e) { console.log(e.name, e.code); }

const bom = new Uint8Array([0xef, 0xbb, 0xbf, 0x41]);
console.log(new TextDecoder().decode(bom).length, new TextDecoder('utf-8', { ignoreBOM: true }).decode(bom).length);
console.log(new TextDecoder('windows-1252').decode(new Uint8Array([0x80, 0xe9])), new TextDecoder('utf-16le').decode(new Uint8Array([0x41, 0, 0x3d, 0xd8, 0x00, 0xde])));

const streaming = new TextDecoder();
console.log(streaming.decode(new Uint8Array([0xe2, 0x82]), { stream: true }) + streaming.decode(new Uint8Array([0xac])));
console.log(decoder.encoding, encoder.encoding);
Output (identical on Node.js 26, 24 and 22)
Uint8Array(6) [ 226, 130, 172, 117, 114, 111 ] { read: 4, written: 6 } Uint8Array(6) [ 226, 130, 172, 117, 114, 111 ]
€
"�"
TypeError ERR_ENCODING_INVALID_ENCODED_DATA
1 2
€é A😀
€
utf-8 utf-8

The first output line is wrapped by console.log printing the same destination array twice: encode() returned a new array, and encodeInto() filled dest (read: 4 code units, written: 6 bytes, because the euro sign takes three). The BOM line shows the default stripping the mark (length 1) and ignoreBOM: true keeping it (length 2). The byte 0x80 is the euro sign in windows-1252, which is not what latin1 (ISO-8859-1) means in Buffer; in the WHATWG standard the labels latin1 and iso-8859-1 are in fact aliases of windows-1252, a difference worth knowing when an HTML or HTTP header names a "latin1" charset.

Table 4. Choosing a text codec
Need Prefer

UTF-8, UTF-16LE, Latin-1, hex, base64 in Node.js-only code

Buffer.from() and buf.toString()

Code shared with browsers and other runtimes

TextEncoder and TextDecoder

Legacy encodings (windows-1252, shift_jis, gbk and so on)

TextDecoder, or buffer.transcode() for the few encodings it supports

Error on invalid input

TextDecoder with fatal: true, or buffer.isUtf8() before decoding

Decode a chunked stream

StringDecoder, a stream encoding option, or TextDecoder with { stream: true }, or a TextDecoderStream on web streams

Blob and File

Blob is the web platform’s immutable chunk of raw bytes with a MIME type. It is created from an array of strings, ArrayBuffer`s, typed arrays, `Buffer`s or other blobs, and is global (also exported from `node:buffer). You read it asynchronously: blob.text(), blob.arrayBuffer(), blob.bytes() (a Uint8Array, added in v22.3.0 and v20.16.0) or blob.stream() (a web ReadableStream), and cut it with blob.slice(start, end, type), which does not copy the data. Because a Blob is immutable it can be posted to worker threads without copying; see Worker Threads. File extends Blob with a name and a lastModified timestamp, and is what FormData entries carry. fs.openAsBlob(path) returns a Blob backed lazily by a file on disk, so a large file can be passed to fetch() or a worker without being read into memory; the file must not change after the blob is created, or reading fails with a DOMException. Node.js 26.10.0 added a synchronous fs.openAsBlobSync(). A Blob is not a stream and not a Buffer: you cannot read it synchronously, so convert it once at an API boundary and work with bytes beyond it.

Blob and File
import { Blob, File } from 'node:buffer';
import { openAsBlob } from 'node:fs';
import { writeFile, rm } from 'node:fs/promises';

const blob = new Blob(['Hello, ', new Uint8Array([0x42, 0x6f, 0x6f, 0x6b]), '!'], { type: 'text/plain' });
console.log(blob.size, blob.type, await blob.text());
console.log(await blob.bytes(), new Uint8Array(await blob.arrayBuffer()).length);
const part = blob.slice(7, 11, 'text/plain');
console.log(await part.text());

const file = new File([blob], 'greeting.txt', { type: 'text/plain', lastModified: 0 });
console.log(file.name, file.size, file.lastModified, file instanceof Blob);

let total = 0;
for await (const chunk of blob.stream()) total += chunk.byteLength;
console.log(total);

await writeFile('sample.bin', Buffer.from('on disk'));
const onDisk = await openAsBlob('sample.bin');      // lazily backed by the file
console.log(onDisk.size, await onDisk.text());
await rm('sample.bin');
console.log(globalThis.Blob === Blob, typeof globalThis.File);
Output (identical on Node.js 26, 24 and 22)
12 text/plain Hello, Book!
Uint8Array(12) [
   72, 101, 108, 108, 111,
   44,  32,  66, 111, 111,
  107,  33
] 12
Book
greeting.txt 12 0 true
12
7 on disk
true function

Other node:buffer helpers

The module has a few small functions that save hand-written validation. buffer.isUtf8(input) and buffer.isAscii(input) report whether a Buffer, ArrayBuffer or typed array holds valid UTF-8 or only 7-bit ASCII, which is cheaper and safer than decoding and looking for replacement characters. buffer.transcode(source, fromEnc, toEnc) converts between 'utf8', 'ucs2'/'utf16le', 'latin1', 'ascii' and 'binary', substituting a replacement character for what the target cannot represent. buffer.INSPECT_MAX_BYTES controls how many bytes console.log() prints for a buffer (50 by default). buffer.resolveObjectURL(id) returns the Blob for a blob:nodedata: URL created with URL.createObjectURL() (stable since v24.0.0 and v22.17.0). The legacy buffer.atob() and buffer.btoa() exist for browser compatibility; use Buffer.from(data, 'base64') and buf.toString('base64') in new code.

Validation and transcoding helpers
import buffer, { Buffer, isUtf8, isAscii, transcode, constants } from 'node:buffer';

console.log(isUtf8(Buffer.from('café')), isUtf8(Buffer.from([0xff])), isAscii(Buffer.from('cafe')), isAscii(Buffer.from('café')));
console.log(transcode(Buffer.from('€'), 'utf8', 'utf16le'), constants.MAX_LENGTH, constants.MAX_STRING_LENGTH);
console.log(typeof buffer.SlowBuffer, typeof buffer.atob, typeof Buffer.allocUnsafeSlow);
console.log(Buffer.from('aGk=', 'base64').toString(), Buffer.from('hi').toString('base64'));
Output on Node.js 26 (Node.js 24 and 22 print function instead of undefined for SlowBuffer)
true false true false
<Buffer ac 20> 9007199254740991 536870888
undefined function function
hi aGk=

Pitfalls

Table 5. Common binary-data mistakes
Mistake Fix

Using allocUnsafe() and reading bytes you did not write

Use Buffer.alloc(), or overwrite every byte (fill(), copy(), write()) before use. Never send a partially filled allocUnsafe() buffer.

Treating buf.length as a character count

It is bytes. Use Buffer.byteLength(str) for the size of a string.

Concatenating string chunks (data += chunk) from a stream

Collect Buffer chunks and decode once, or use StringDecoder or the stream’s encoding option.

Converting binary data through toString() (UTF-8)

The conversion is lossy. Use 'base64' or 'hex', or keep the bytes.

Passing buf.buffer to an API that wants only your bytes

A pooled buffer shares a larger ArrayBuffer; pass buf.subarray(…​) or copy with Buffer.from(buf), or use allocUnsafeSlow().

Expecting slice() to copy

It does not on Buffer, and it is deprecated. Use subarray() for a view and Buffer.from(buf) for a copy.

Using Buffer.from(typedArray) to get its bytes

It copies element values, truncated to bytes. Use Buffer.copyBytesFrom() or wrap view.buffer.

Accepting a size from untrusted input and allocating it

Validate and cap it. A large allocation is a denial of service, and new Buffer(n) made the type confusion worse.

Comparing secrets with buf.equals() or ===

Use a constant-time comparison; see Crypto, Passwords & Secrets.

Trusting hex or base64 decoding to reject bad input

Buffer.from(string, 'hex') truncates silently. Validate the format, or use the strict native helpers where they exist.

What changed recently

Table 6. Changes relevant to binary data
Version Change

v26.10.0

fs.openAsBlobSync() added. Uint8Array base64 and hex helpers are available through V8 14.6.

v26.8.0

Buffer.allocUnsafe() and Buffer.allocUnsafeSlow() accept an alignment argument; isUtf8() and isAscii() treat detached `ArrayBuffer`s (and views over them) as empty.

v26.3.0 and v24.18.0

Buffer.poolSize default raised from 8192 to 65536, which makes pooled buffers share larger blocks.

v25.0.0

SlowBuffer removed (DEP0030, End-of-Life), after its runtime deprecation in v24.0.0.

v24.0.0 and v22.17.0

fs.openAsBlob() and buffer.resolveObjectURL() marked stable.

v22.3.0

blob.bytes() added.

v22.0.0

buffer.constants.MAX_LENGTH is 2^53 - 1 on 64-bit builds.

Where the books are outdated, narrower or silent

Samer Buna’s Efficient Node.js (O’Reilly, 2025) explains streams at length but treats Buffer and typed arrays only in passing, so the material on this page (the pool, subarray() sharing, DataView, TextDecoder, Blob) has to come from the official documentation. Jonathan Wexler’s Node.js Projects (O’Reilly, 2025) uses Buffer.from(JSON.stringify(…​)) to turn queue payloads into bytes and toString('hex') to print random salt bytes; both are correct current idioms, but the book’s queue and password examples are covered by the pages on messaging and cryptography because their problems are not about buffers. Neither book mentions that SlowBuffer is gone (removed in v25.0.0), that buf.slice() is deprecated, that the pool size changed in v26.3.0 and v24.18.0, or the Web-standard TextDecoder, Blob and File; the current approach in each case is the section above.