Base64 shows up everywhere: data URLs, JWTs, email attachments, API tokens, config files that need to embed binary data as text. And yet a surprising number of developers treat it as some kind of black box or, worse, mistake it for encryption. It's neither magic nor security. It's a simple, predictable way to represent binary data using only printable ASCII characters.

What Base64 actually does

Computers store everything as bytes, but plenty of systems were designed to move text around safely — email headers, URLs, JSON strings, XML. Drop raw binary into those channels and things break: null bytes get truncated, high-bit characters get mangled, control characters do unexpected things.

Base64 solves this by mapping every 3 bytes of input (24 bits) onto 4 characters drawn from a 64-character alphabet: A–Z, a–z, 0–9, plus + and /. Since 64 is 2⁶, each output character encodes exactly 6 bits. When the input length isn't a multiple of 3, the output is padded with = characters to keep the length a multiple of 4.

The important consequence: Base64 output is always about 33% larger than the input. Three bytes become four characters. It's a text-safe representation, not a compressor.

When you'd actually use it

  • Embedding binary in text formats — small images or fonts as data URLs in CSS/HTML, or binary blobs inside JSON where you can't send raw bytes.
  • Email attachments — MIME has used Base64 for decades to ship binary files over a text-only transport.
  • Encoding credentials — HTTP Basic Auth sends username:password as Base64 in the Authorization header.
  • Tokens and JWTs — the header and payload of a JWT are Base64URL-encoded JSON (more on that variant below).

When NOT to use it

If you're reaching for Base64 to hide data, stop. It's trivially reversible — anyone can decode it in one line of code. HTTP Basic Auth credentials are Base64, which is exactly why Basic Auth must only ever run over HTTPS. Base64 is encoding, not encryption.

Also think twice before Base64-encoding large files into JSON. That 33% size penalty adds up fast, and you lose the ability to stream. For big payloads, upload the binary directly or use multipart requests.

A worked example

Let's encode the string Man by hand, because seeing the bit-shuffling once makes the whole thing click.

Text:     M         a         n
ASCII:    77        97        110
Binary:   01001101  01100001  01101110

Regroup into 6-bit chunks:
          010011  010110  000101  101110
Decimal:  19      22      5       46
Base64:   T       W       F       u

Result:   "Man" -> "TWFu"

Now try a case that needs padding. The single byte M (8 bits) can't fill even two 6-bit chunks cleanly:

"M"  -> "TQ=="    (one byte, two padding chars)
"Ma" -> "TWE="    (two bytes, one padding char)

In practice you won't do this by hand. Every language ships Base64 built in:

// JavaScript (browser / Node)
btoa("Man");                // "TWFu"
atob("TWFu");               // "Man"

// Node with binary-safe Buffer
Buffer.from("Man").toString("base64");        // "TWFu"
Buffer.from("TWFu", "base64").toString();     // "Man"

// Python
import base64
base64.b64encode(b"Man")    # b'TWFu'
base64.b64decode(b"TWFu")   # b'Man'

When you just need to eyeball a token or convert a small blob without opening an editor, a quick Base64 Encoder / Decoder is faster than firing up a REPL — paste, encode or decode, done.

The gotchas that bite people

btoa/atob and Unicode

The browser's btoa only handles Latin-1 (bytes 0–255). Feed it an emoji or a non-Latin character and it throws InvalidCharacterError. The fix is to encode to UTF-8 bytes first:

// Safe UTF-8 Base64 in the browser
function encodeBase64(str) {
  const bytes = new TextEncoder().encode(str);
  const bin = String.fromCharCode(...bytes);
  return btoa(bin);
}
encodeBase64("café ☕");

Base64URL vs standard Base64

The standard alphabet uses + and /, which are unsafe in URLs and filenames. The Base64URL variant swaps them for - and _, and usually drops the trailing = padding. This is what JWTs use. If you decode a JWT payload with a plain Base64 decoder and it fails, mismatched alphabets or missing padding is almost always why. You may need to re-add padding:

def add_padding(s):
    return s + "=" * (-len(s) % 4)

Line wrapping

Some encoders (looking at you, MIME and OpenSSL) insert a newline every 64 or 76 characters. That's valid for those specs but will break a strict decoder that doesn't expect whitespace. If a decode fails on "clean-looking" data, strip whitespace first.

Assuming it's smaller

Worth repeating because people keep getting surprised: Base64 makes data bigger, not smaller. If you're Base64-encoding compressed data, compress first, then encode. Encoding then compressing largely wastes the compressor's time on high-entropy text.

A quick mental model

Think of Base64 as a translator that turns arbitrary bytes into a language every text channel speaks fluently. It's lossless, deterministic, and reversible by anyone. Use it to move binary through text-shaped pipes. Don't use it for secrecy, don't use it to save space, and always know whether you're dealing with the standard or URL-safe alphabet.

Get those distinctions right and Base64 stops being a source of mysterious bugs and becomes what it should be: a boring, reliable tool you barely think about.

Photo by Jake Walker on Unsplash