Base64 is one of those things you copy-paste from Stack Overflow, see in API responses, and generally trust without thinking too hard about it. That works fine right up until it doesn't — until you're debugging a mangled image, a broken JWT, or a config value that refuses to decode. So let's actually unpack what base64 is and where it belongs.

What base64 actually is

Base64 is a binary-to-text encoding. It takes arbitrary bytes and represents them using 64 printable ASCII characters: A–Z, a–z, 0–9, plus + and /. The = character is used for padding.

The mechanics are simple. It chews through your input three bytes at a time. Three bytes is 24 bits, which splits neatly into four 6-bit chunks. Each 6-bit chunk (a value from 0 to 63) maps to one character in the base64 alphabet. If your input isn't a multiple of three bytes, the encoder pads the output with = so the length is always a multiple of four.

That 3-to-4 ratio is why base64 output is always about 33% larger than the input. You're trading size for safety.

It is not encryption

This is the single most important thing to internalise. Base64 is not encryption, not hashing, and not compression. Anyone can decode it in one line of code. If you base64-encode a password or an API key and think you've secured it, you haven't — you've just made it slightly less readable to a human skimming logs. Treat base64 strings as plaintext.

When you'd actually reach for it

Base64 exists to move binary data through channels that were designed for text. A few real-world cases:

  • Email attachments. MIME uses base64 to stuff binary files into messages that are fundamentally text-based. This is the original use case.
  • Data URIs. Embedding a small image or font directly in CSS or HTML: background: url(data:image/png;base64,iVBOR...). Handy for tiny assets to avoid an extra HTTP request.
  • JSON and config files. JSON can't hold raw bytes, so if you need to ship a binary blob inside a JSON payload, you base64 it first.
  • JWTs. The header and payload of a JSON Web Token are base64url-encoded. This is why you can paste a JWT into a decoder and read its claims — they were never secret.
  • Basic Auth headers. Authorization: Basic is just base64(username:password). Which, again, is why Basic Auth over plain HTTP is a terrible idea.

The common thread: text-only transport, binary payload. If both sides can handle raw bytes, you probably don't need base64 at all.

A quick worked example

Let's encode the string Hi!. In ASCII that's three bytes: 72 105 33, or in binary:

H        i        !
01001000 01101001 00100001

Concatenate those 24 bits and regroup into four 6-bit chunks:

010010 000110 100100 100001
   18     6      36     33

Map each value to the base64 alphabet (0=A, 1=B, …) and you get SGkh. Since Hi! is exactly three bytes, there's no padding.

In practice you'll let the standard library do this. JavaScript in the browser and Node both have it, though they differ:

// 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 want to sanity-check a value — decode a JWT payload, inspect what's really in a config string, or confirm an encoding round-trips — I don't bother opening a REPL. A quick paste into Devsly's Base64 Encoder / Decoder gets me the answer in a second, and it runs in the browser so I'm not pasting sensitive-ish strings into some random server.

The gotchas that bite people

btoa chokes on Unicode

The browser's btoa only handles characters in the Latin-1 range. Hand it an emoji or a Chinese character and it throws InvalidCharacterError. The fix is to encode to UTF-8 bytes first:

// Safe for Unicode in the browser
function encodeUtf8Base64(str) {
  const bytes = new TextEncoder().encode(str);
  const bin = String.fromCharCode(...bytes);
  return btoa(bin);
}

Node's Buffer handles UTF-8 correctly by default, so this bug tends to surprise people who move code between environments.

Standard vs URL-safe base64

Standard base64 uses + and /. Both are special characters in URLs and in some filenames. base64url swaps them for - and _, and usually drops the = padding. JWTs use this variant. If you decode a JWT with a standard decoder and it fails, a stray - or missing padding is often why. Make sure your encoder and decoder agree on the variant.

Padding and whitespace

Some decoders are strict about the = padding being present and correct; others are forgiving. Likewise, line breaks: MIME base64 traditionally wraps at 76 characters, and a decoder that doesn't strip newlines will fail on that input. If a decode blows up, check for whitespace before blaming the data.

Don't base64 huge files for no reason

That 33% size bump is real. Embedding a 2 MB image as a data URI means shipping ~2.7 MB of text that can't be cached separately and bloats your HTML or CSS. Base64 is great for tiny inline assets; for anything substantial, serve the binary file over HTTP and let the browser cache it.

The short version

Base64 is a transport format, not a security measure. Use it when you need to squeeze bytes through a text-only channel — JSON, email, data URIs, headers — and skip it when raw binary is an option. Watch out for Unicode handling, pick the right variant (standard vs URL-safe), and remember that anyone can read what you encode. Keep those three things straight and base64 stops being mysterious and becomes just another tool in the box.

Photo by Markus Spiske on Unsplash