Sooner or later every developer meets Base64: in a JWT, a data URL, an email attachment, an API key file. And sooner or later most developers misuse it — treating it as encryption (it isn't), embedding megabytes of images with it (don't), or not recognizing it when it appears (it's everywhere once you can spot it). This is the explanation we wish we'd read early on.
The problem Base64 solves
Many of computing's oldest and most important channels were designed for text, not arbitrary bytes: email protocols, URLs, JSON documents, XML, HTTP headers. Send raw binary through them and things break — a byte that happens to equal a quote character terminates a string early; email gateways mangle bytes outside the printable range.
Base64 is the standard workaround: it re-expresses any byte sequence using only 64 universally-safe characters — A–Z, a–z, 0–9, + and / (with = for padding). The mechanism: take the input 3 bytes (24 bits) at a time, split into four 6-bit groups, and map each group to one of the 64 characters. When the input isn't a multiple of 3 bytes, = padding marks the shortfall.
Text: Hi!
Bytes: 01001000 01101001 00100001
6-bit: 010010 000110 100100 100001
Base64: S G k h → "SGkh"
That's the entire trick. Two properties follow directly, and they're the ones that matter in practice:
- It is perfectly reversible by anyone. No key, no secret — decoding is a table lookup.
- It inflates data by 33%. Every 3 bytes become 4 characters, always.
Base64 is not encryption — and this causes real breaches
Base64 output looks scrambled to humans (cGFzc3dvcmQxMjM=), which tempts people into using it to "hide" secrets. It hides nothing: any browser console decodes it with atob(), and any attacker's first reflex on seeing a Base64-looking string is to decode it. Credentials "protected" this way in config files, URLs, or client-side code are effectively plaintext. The rule is absolute: Base64 is transport encoding, never protection. If data must be confidential, encrypt it with a real algorithm (AES-GCM and the like) — and note that a JWT's payload is plain Base64-encoded JSON too, which is why you never put secrets inside a token that clients can read.
Where Base64 is the right tool
- Email attachments — the original use case: MIME encodes binary attachments as Base64 to survive text-only mail infrastructure.
- Binary values inside JSON or XML — JSON has no bytes type, so a thumbnail, a cryptographic signature, or a protobuf blob inside a JSON API travels as a Base64 string.
- Small images inlined in CSS or HTML via data URLs:
url('data:image/png;base64,iVBORw0…')— saves an HTTP request for icons in the low-kilobyte range. - Basic HTTP authentication —
Authorization: Basic <base64(user:pass)>. Note this is framing, not security; the security comes entirely from the TLS connection it travels over. - Binary data in URLs — using the URL-safe variant (
-and_replacing+and/), because standard Base64's characters collide with URL syntax:+means space in query strings and/is the path separator. If you've ever had "works usually, fails on some inputs" URL bugs with encoded data, a variant mismatch is the likely cause.
Where it's the wrong tool
- Large images on web pages. A 900 KB photo becomes 1.2 MB of Base64 that can't be cached separately from the page, can't be lazy-loaded, and blocks parsing. The data-URL technique is for small icons; past ~10 KB, a normal image URL wins on every metric.
- Files in a database. Storing uploads as Base64 text columns adds 33% storage, breaks streaming, and slows every query touching the row. Store files on disk or object storage and keep the path in the database.
- Anything where the goal is secrecy. Covered above, but it bears repeating because it keeps appearing in security audits.
Recognizing and working with it
You can spot probable Base64 by sight: only letters, digits, +// (or -/_), length divisible by 4, possibly ending in one or two =. To inspect one, every environment has a one-liner — atob()/btoa() in the browser, base64_decode() in PHP, base64.b64decode() in Python, or paste it into our Base64 encoder/decoder, which converts both directions entirely in your browser — nothing you paste is sent anywhere, which matters precisely because people so often decode tokens and credentials.
One gotcha worth knowing: Base64 operates on bytes, so text with non-ASCII characters must be encoded to UTF-8 bytes first. JavaScript's legacy btoa() predates this understanding and throws on characters outside Latin-1 — the modern pattern goes through TextEncoder first. If you've seen InvalidCharacterError from btoa("café"), that's why.
Base64 is a fine example of infrastructure that works so well it became invisible — a 1980s solution to text-only channels that now rides inside every JWT, every email, and half the APIs on the internet. Use it for transport, never for secrets, and mind the 33%.