Base64 is one of those things every developer touches but few stop to understand. You see it in data URIs, JWT tokens, email attachments, and API responses. It looks like gibberish, but it's completely predictable once you know the rules.

What Base64 actually is

Base64 is an encoding scheme that represents binary data using 64 printable ASCII characters: A-Z, a-z, 0-9, plus + and /, with = used for padding. That's it. It is not encryption, compression, or hashing. Anyone can decode it in a second. Treating it as a security measure is the single most common mistake I see.

The mechanism is simple. It takes three bytes (24 bits) at a time and splits them into four 6-bit groups. Each 6-bit group maps to one character in the 64-character alphabet. Three bytes in, four characters out — which is why Base64 output is always about 33% larger than the original.

Why 6 bits?

Six bits give you 2^6 = 64 possible values, hence the name. The whole point is to stay within a safe, printable character set that won't get mangled by systems that were designed for text and might choke on raw binary.

When you'd actually use it

The core use case is moving binary data through a text-only channel. Some concrete examples:

  • Data URIs — embedding a small image directly in HTML or CSS with data:image/png;base64,... to save an HTTP request.
  • Email attachments — MIME uses Base64 because the old SMTP protocol only reliably handled 7-bit ASCII.
  • JSON payloads — JSON has no native binary type, so you Base64-encode a file or a byte blob to send it as a string.
  • Tokens and credentials — HTTP Basic Auth encodes user:pass as Base64, and JWTs Base64url-encode their segments.
  • Storing binary in text-based configs — like a certificate or key pasted into a YAML file.

The rule of thumb: if the transport or storage expects text and you have bytes, Base64 is the bridge. If both ends handle binary cleanly, skip it — you're just adding 33% overhead for nothing.

A short worked example

Let's encode the string Hi!. It's three bytes, which is convenient because it maps to exactly four Base64 characters with no padding.

Text:    H         i         !
ASCII:   72        105       33
Binary:  01001000  01101001  00100001

Regroup into 6-bit chunks:
         010010  000110  100100  100001
Decimal: 18      6       36      33
Base64:  S       G       k       h

Result: SGkh

Now the padding case. Encode just Hi (two bytes). Two bytes is 16 bits, which splits into three 6-bit groups with 2 bits left over. The encoder pads the last group with zeros and appends a single = to signal one missing byte:

echo -n "Hi" | base64
# SGk=

echo -n "H" | base64
# SA==

One = means the final group encoded two bytes; two = means it encoded one. That padding is how the decoder knows where the real data ends.

In JavaScript, the browser and Node give you helpers directly:

// Browser
btoa("Hi!");        // "SGkh"
atob("SGkh");       // "Hi!"

// Node.js
Buffer.from("Hi!").toString("base64");           // "SGkh"
Buffer.from("SGkh", "base64").toString("utf8");  // "Hi!"

When I'm debugging a token or checking what's inside a data URI, I don't bother writing a script — I paste it into a Base64 Encoder / Decoder and read it instantly.

Common gotchas

btoa and Unicode

The browser's btoa() only handles Latin-1 characters. Feed it an emoji or a non-Latin string and it throws InvalidCharacterError. You need to encode to UTF-8 bytes first:

// This breaks:
btoa("café ☕"); // Error

// This works:
btoa(String.fromCharCode(...new TextEncoder().encode("café ☕")));

Node's Buffer doesn't have this problem because it works on bytes and lets you specify the charset explicitly.

Base64 vs Base64url

Standard Base64 uses + and /, which are unsafe in URLs and filenames. The URL-safe variant swaps them for - and _ and often drops the padding. JWTs use Base64url. If you decode a JWT with a plain Base64 decoder and it fails, mismatched alphabets are usually why.

It is not security

Worth repeating because it keeps happening. Base64-encoding a password, an API key, or PII gives you exactly zero protection. It's obfuscation at best, and not even that. Encode for transport; encrypt for secrecy. Those are different jobs.

The size overhead

That ~33% growth matters at scale. Inlining a 200 KB image as a data URI turns it into roughly 267 KB of markup, and it can't be cached separately from the page. For anything but tiny assets, a normal file reference is better.

Whitespace and line breaks

Some encoders (MIME, PEM certificates) insert line breaks every 64 or 76 characters. Some decoders reject unexpected whitespace. If a decode fails on data that looks valid, strip the newlines and try again.

Quick mental model

Keep these in your head and you'll rarely get caught out:

  • Base64 = binary made safe for text channels, nothing more.
  • Output is always a multiple of 4 characters, sometimes ending in = or ==.
  • It grows your data by about a third.
  • Watch the alphabet: standard vs URL-safe.
  • Encode UTF-8 explicitly for non-ASCII text in the browser.

It's a humble tool, but knowing how it works turns a lot of "why is my token failing" head-scratchers into a five-second fix. Once you can eyeball a string, spot the padding, and recognize the URL-safe variant, most Base64 bugs stop being bugs at all.

Photo by Mohammad Rahmani on Unsplash