Base64 is one of those things most developers use without ever stopping to think about what it does. You paste an image into a data URL, you decode a JWT payload, you see a long string of random-looking characters in an API response — all Base64. It's worth understanding properly, because misusing it causes bugs that are genuinely annoying to track down.

What Base64 actually is

Base64 is an encoding scheme, not encryption and not compression. It takes binary data and represents it using only 64 printable ASCII characters: A-Z, a-z, 0-9, plus + and /, with = used for padding.

The mechanism is simple. It reads your input three bytes at a time — that's 24 bits — and splits those 24 bits into four groups of 6 bits. Each 6-bit group maps to one character in the Base64 alphabet. Three bytes in, four characters out.

That ratio is the whole point to remember: Base64 output is roughly 33% larger than the input. Every three bytes become four characters. If your input length isn't a multiple of three, the encoder pads the end with = so the output is always a multiple of four characters.

Why encode binary as text at all?

Plenty of systems were built to handle text, not arbitrary bytes. Email (via MIME), JSON, XML, HTTP headers, URLs — these channels either can't carry raw binary safely or will mangle certain byte values. Base64 gives you a way to stuff binary data through a text-only pipe and get it back out intact on the other side.

When to reach for Base64

  • Embedding small assets in code — tiny icons or fonts as data URLs so the browser doesn't make an extra HTTP request.
  • Email attachments — MIME uses Base64 under the hood to encode files.
  • API payloads — sending a file or a binary blob inside a JSON body when you can't use multipart uploads.
  • Tokens and credentials — JWTs Base64-encode their header and payload; HTTP Basic Auth Base64-encodes user:password.
  • Storing binary in text-only fields — some config formats or databases where a text column is all you've got.

When NOT to use it

Don't Base64-encode things for the sake of it. That 33% size bloat matters for large files — a 10 MB upload becomes ~13.3 MB on the wire. For anything sizeable, prefer multipart form uploads or a direct binary transfer.

And please stop treating Base64 as a security measure. Anyone can decode it in a second. HTTP Basic Auth being Base64 is exactly why it must always ride over HTTPS — the encoding hides nothing.

A quick worked example

Let's encode the string Hi! by hand to see the mechanism.

Characters:  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"

Three bytes in, four characters out, no padding needed because the length was already a multiple of three. In most languages you'll never do this by hand, of course:

// JavaScript (browser)
btoa("Hi!");            // "SGkh"
atob("SGkh");           // "Hi!"

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

# Python
import base64
base64.b64encode(b"Hi!")        # b'SGkh'
base64.b64decode("SGkh")        # b'Hi!'

When I just need to check a value quickly without opening a REPL, I use an in-browser Base64 Encoder / Decoder — handy for inspecting a JWT payload or sanity-checking an encoded string from a log.

Common gotchas

btoa and Unicode don't mix cleanly

The classic browser trap: btoa only handles characters in the Latin-1 range. Feed it an emoji or an accented character and it throws. If your string has Unicode, encode to UTF-8 bytes first:

// Breaks:
btoa("café 🎉");  // InvalidCharacterError

// Works:
btoa(unescape(encodeURIComponent("café 🎉")));

// Modern approach:
const bytes = new TextEncoder().encode("café 🎉");
btoa(String.fromCharCode(...bytes));

Standard vs URL-safe Base64

Standard Base64 uses + and /, which have special meaning in URLs and get percent-encoded or corrupted. URL-safe Base64 swaps them for - and _. JWTs use the URL-safe variant and also strip the trailing = padding. If you decode a JWT with a plain decoder and it chokes, that's usually why — you need to add padding back and translate the characters.

# Python, decoding a JWT segment
import base64
seg = "eyJhbGciOiJIUzI1NiJ9"
padded = seg + "=" * (-len(seg) % 4)
base64.urlsafe_b64decode(padded)

Padding assumptions

Some decoders demand correct padding; others don't care. If you strip or mishandle the = characters, you may get an error or silently truncated output. When in doubt, re-pad the string to a multiple of four before decoding.

Line breaks in encoded output

MIME-style Base64 wraps lines at 76 characters. If you're parsing output from a tool like OpenSSL or an email header, you may need to strip newlines before decoding, or your decoder might reject the whitespace.

Treating it as the file itself

A data URL like data:image/png;base64,iVBOR... has a prefix before the comma. The Base64 part is only what comes after it. Forgetting to split on the comma is a surprisingly common source of "why won't my image load" bugs.

The short version

Base64 is a transport convenience: it makes binary data survive text-only channels at the cost of ~33% size. It's not security, it's not compression, and it's reversible by anyone. Keep an eye on the Unicode handling in browsers, know whether you need the standard or URL-safe alphabet, and respect the padding rules. Get those three right and Base64 will quietly do its job without ever becoming a problem.

Photo by Markus Spiske on Unsplash