Buffers & Binary Data

Reviewed & published by Brayan K

A Buffer is Node's fixed-length container for raw bytes, the building block it uses to handle files, network data, and any binary content that doesn't fit neatly into a text string.

Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll be able to create buffers with from and alloc, convert between bytes and text in utf8, hex, and base64, read individual bytes, join buffers with concat, and understand why stream chunks arrive as Buffers.

What You'll Learn in This Lesson

1️⃣ Creating Buffers

There are two main ways to make a buffer. Buffer.from(string) takes text and encodes it as bytes (utf8 by default). Buffer.alloc(n) makes an empty, zero-filled buffer of n bytes that you can fill in yourself.

When you log a buffer, Node shows its bytes as hex (like <Buffer 48 65 6c 6c 6f>). You can index it like an array to read one byte, ask for its .length in bytes, and call .toString('utf8') to read it back as text.

// A Buffer is a fixed-length chunk of raw bytes. Node uses them for
// files, network data, and anything that isn't plain text.

// Buffer.from(string) encodes the text as bytes (utf8 by default).
const buf = Buffer.from('Hello');
console.log(buf);                  // bytes shown in hex
console.log(buf.length);           // 5 bytes (one per ASCII char)
console.log(buf.toString('utf8')); // decode back to a string
console.log(buf[0]);               // first byte: 'H' is 72

// Buffer.alloc(n) makes a zero-filled buffer of n bytes — safe and clean.
const zeros = Buffer.alloc(4);
console.log(zeros);

2️⃣ Encodings: utf8, hex & base64

The same bytes can be displayed in different encodings. .toString('hex') shows each byte as two hex digits, handy for debugging. .toString('base64') packs the bytes into a compact, text-safe form often used for tokens, data URLs, and email attachments.

Going the other way, Buffer.from(str, 'base64') (or 'hex') decodes an encoded string back into the original bytes, which you can then read as 'utf8' text.

// The SAME bytes can be viewed in different encodings.
const buf = Buffer.from('Node.js');
console.log(buf.toString('hex'));     // each byte as two hex digits
console.log(buf.toString('base64'));  // compact text-safe encoding

// Decode base64 back into readable text:
const decoded = Buffer.from('SGVsbG8=', 'base64');
console.log(decoded.toString('utf8'));   // -> Hello

// You can index a buffer like an array to read individual bytes:
const bytes = Buffer.from('AB');
console.log(bytes[0], bytes[1]);   // 65 66  (ASCII for A and B)

3️⃣ Joining Buffers & Streams

Because each buffer has a fixed size, you don't append to one — you combine several with Buffer.concat([a, b]), which returns one new buffer containing all the bytes in order.

This is the exact pattern you use with streams: data arrives in pieces called chunks (each a Buffer), you push them into an array, and when the stream ends you Buffer.concat them into a single buffer to read as text or parse.

// Buffers have a fixed size, so to "join" them you use Buffer.concat,
// which takes an ARRAY of buffers and returns one new buffer.
const a = Buffer.from('Hello, ');
const b = Buffer.from('world!');
const joined = Buffer.concat([a, b]);
console.log(joined.toString('utf8'));   // -> Hello, world!
console.log(joined.length);             // -> 13

// This is exactly how you collect data from a stream: push each chunk
// into an array, then concat them all once the stream ends.
const chunks = [];
chunks.push(Buffer.from('one '));
chunks.push(Buffer.from('two '));
chunks.push(Buffer.from('three'));
console.log(Buffer.concat(chunks).toString('utf8'));

Your turn. Fill in the three blanks marked ___, then run it and compare with the expected output.

// 🎯 YOUR TURN — fill in each ___ then run it.

// 1) Make a buffer from the text 'Hi'
const buf = Buffer.from(___);          // 👉 the string 'Hi'

// 2) Print how many BYTES it holds
console.log(buf.___);                  // 👉 the length property

// 3) Print the buffer as a HEX string
console.log(buf.toString(___));        // 👉 the encoding 'hex'

// ✅ Expected output:
//    2
//    4869

Mini-Challenge: A Base64 Round-Trip

No blanks this time — just a brief and an outline. Encoding a string to base64 and decoding it back is the foundation of countless real tasks (tokens, data URLs, basic auth headers). Write it yourself, run it, and check your output.

// 🎯 MINI-CHALLENGE: a base64 round-trip
// 1. Start with the string 'secret'.
// 2. Encode it to base64 (Buffer.from(text).toString('base64')).
//    Print the base64 string.
// 3. Decode that base64 back to utf8 text and print it — you should
//    get 'secret' again.
//
// ✅ Expected output:
//    c2VjcmV0
//    secret

// your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Buffers

TaskBuffer Syntax
From textBuffer.from('hi')
Empty, zero-filledBuffer.alloc(8)
To textbuf.toString('utf8')
To hexbuf.toString('hex')
To base64buf.toString('base64')
From base64Buffer.from(s, 'base64')
Join buffersBuffer.concat([a, b])

🎉 Lesson Complete!

Practice quiz

What is a Buffer in Node.js?

  • A resizable string
  • A network socket
  • A fixed-length block of raw bytes
  • A JSON object

Answer: A fixed-length block of raw bytes. A Buffer is a fixed-length chunk of raw bytes, used for files, network data, and binary content.

What does Buffer.from('Hello') create?

  • A buffer of the utf8 bytes for 'Hello'
  • The number 5
  • A base64 string
  • An empty buffer

Answer: A buffer of the utf8 bytes for 'Hello'. Buffer.from(string) encodes the text as bytes, utf8 by default, so 'Hello' becomes 5 bytes.

What is buf.length measured in?

  • Characters
  • Bits
  • Words
  • Bytes

Answer: Bytes. A buffer's length is the number of bytes, which may exceed the visible character count for multi-byte text.

What does Buffer.alloc(4) produce?

  • A buffer with random leftover memory
  • A zero-filled buffer of 4 bytes
  • A 4-character string
  • An error

Answer: A zero-filled buffer of 4 bytes. Buffer.alloc(n) creates an n-byte buffer that is zero-filled and safe to read immediately.

How does Buffer.alloc differ from Buffer.allocUnsafe?

  • alloc zero-fills the memory; allocUnsafe may contain leftover data
  • They are identical
  • allocUnsafe is always zero-filled
  • alloc is faster

Answer: alloc zero-fills the memory; allocUnsafe may contain leftover data. allocUnsafe skips zeroing for speed but may expose leftover memory; use alloc by default.

What does buf.toString('hex') return?

  • The base64 form
  • The original text
  • Each byte as two hex digits
  • The byte count

Answer: Each byte as two hex digits. .toString('hex') shows each byte as two hexadecimal digits, handy for debugging.

Is base64 a form of encryption?

  • Yes, it hides secrets
  • No, it is encoding that anyone can decode
  • Yes, but only with a key
  • No, it is compression

Answer: No, it is encoding that anyone can decode. base64 is encoding, not encryption. Anyone can decode it; it is for moving binary data through text channels.

How do you combine two buffers into one?

  • a + b
  • a.push(b)
  • Buffer.join(a, b)
  • Buffer.concat([a, b])

Answer: Buffer.concat([a, b]). Buffers have a fixed size, so you combine several with Buffer.concat, which takes an array of buffers.

What does Buffer.concat([Buffer.from('Hello, '), Buffer.from('world!')]) have for .length?

  • 12
  • 13
  • 2
  • 6

Answer: 13. 'Hello, ' is 7 bytes and 'world!' is 6 bytes, so the joined buffer is 13 bytes.

By default, what type is each chunk delivered by a readable stream?

  • A string
  • A number
  • A Buffer
  • An array

Answer: A Buffer. Stream chunks are Buffers by default; you collect them and Buffer.concat at the end unless you set an encoding.

Continue this course

Frequently asked questions

What exactly is a Buffer in Node.js?

A Buffer is a fixed-length block of raw memory holding bytes, sitting outside the normal V8 JavaScript heap. Strings are great for text, but files, images, network packets, and compressed data are sequences of bytes — and that is what a Buffer represents. You create one with Buffer.from() or Buffer.alloc(), index it like an array to read bytes, and convert to and from text with toString(encoding).

Why does Buffer.from('Hello') print <Buffer 48 65 6c 6c 6f> instead of the text?

That hex dump is just how Node displays a Buffer's contents: 48 is the byte for 'H', 65 for 'e', and so on. The buffer holds the right data — it is only the console representation that is hex. To see the text, call buf.toString('utf8'), which decodes the bytes back into a string.

What is the difference between Buffer.alloc and Buffer.allocUnsafe?

Buffer.alloc(n) creates an n-byte buffer that is zero-filled, so it is safe to read immediately. Buffer.allocUnsafe(n) is faster because it skips the zeroing step, but the memory may contain leftover data from earlier use, which could leak sensitive information if you read before fully overwriting it. Use alloc by default; only reach for allocUnsafe when you will immediately fill every byte and you have measured that the speed matters.

How do Buffers relate to streams?

Streams deliver data in pieces, and each piece (chunk) is a Buffer by default. The standard pattern is to collect those chunks into an array as they arrive, then call Buffer.concat(chunks) once the stream ends to get a single buffer, which you can finally turn into a string or parse. If you set an encoding on the stream, it will hand you decoded strings instead of Buffers.