Base64 Demystified: When to Use It, When NOT to Use It
I have read variations of this Slack message about a hundred times: "I'm Base64-encoding the API key in the env var so it's not exposed in plain text." And I think, every time: please stop. Base64 has zero cryptographic properties. Anyone with the encoded value can decode it in one second. You haven't hidden anything. You've just made it slightly less googlable.
The other one I get: "I'm Base64-encoding the JSON before sending it, to save bandwidth." This is even worse, because Base64 makes your payload larger by 33%. The exact opposite of compression. Whoever told you Base64 saves bandwidth was, charitably, mistaken.
This post is the closure on both myths, plus the actual cases where Base64 is the right tool. Spoiler: there are fewer than you think.
What Base64 is, exactly
Base64 takes binary data — any sequence of bytes — and re-encodes it as a string of 64 ASCII characters: A-Z, a-z, 0-9, +, and /. Three input bytes (24 bits) become four output characters (4 × 6 bits = 24 bits). The output is bigger than the input by a factor of 4/3 (≈33%), padded with = if the input length isn't a multiple of 3.
That's the whole thing. A 1:1 reversible mapping from arbitrary bytes to a safe-to-transmit string. No compression. No encryption. No integrity check. The only purpose is transport through systems that only handle text. If you're using it for anything else, you're holding it wrong.
When Base64 is the right tool
There's a specific shape of problem Base64 solves well:
- You have binary data (image, file, encrypted blob, hash, key).
- You need to put it through a text-only transport (URL, JSON, email, HTML, env var).
- The size overhead doesn't matter much.
Concrete cases:
1. Embedding data in JSON
JSON has no native binary type. If you have a public key, an avatar thumbnail, or any binary blob to ship in JSON, Base64 is the standard:
{
"publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgK...",
"avatarThumbnail": "iVBORw0KGgoAAAANSUhEUgAA..."
}
The 33% bloat is the price of round-tripping through any JSON pipeline.
2. Data URLs
For tiny inline images in CSS or HTML:
.icon {
background: url("data:image/svg+xml;base64,PHN2ZyB...");
}
Inlining saves an HTTP request. The 33% bloat is fine for small icons (<2KB). For anything larger, ship the file separately, the savings flip.
3. URL-safe identifiers
Base64URL (a variant using -_ instead of +/, no padding) is what JWTs and many API tokens use:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.dBjftJeZ4CVP-mB92K27uhbUJU1p
Three Base64URL segments separated by dots. The middle segment, decoded, is your JWT payload. Decode it here if you don't believe me.
4. Email attachments (MIME)
Every attachment in every email since 1996 has been Base64-encoded. RFC 2045 defines this. You don't need to do anything yourself — nodemailer, smtplib, every email library handles it, but if you're ever debugging a raw email, the giant blob at the bottom is Base64.
5. Basic authentication
Authorization: Basic dXNlcjpwYXNz is user:pass in Base64. Not encryption. Not even mild obfuscation, anyone with the request can decode it in 0.1 seconds. Basic Auth is only safe over HTTPS.
When Base64 is the wrong tool
People reach for Base64 in three situations where it's actively harmful:
Wrong: as a security measure
"Let's Base64-encode the API key in the env var so it's not exposed in plain text."
It's still exposed. echo "QUJDREVGRw==" | base64 -d reverses it in a millisecond. Base64 has zero cryptographic properties. If you want to hide a value, encrypt it.
This is so common that some compliance tools flag Base64-looking strings near words like "key" or "secret" as a false positive obfuscation attempt: i.e., "this looks like someone tried to hide a secret with Base64, please stop."
Wrong: to "compress" data
"Let's Base64 the JSON before sending it, to save bandwidth."
This makes the payload larger by 33%. You're confusing "encoding" (reversible byte transformation) with "compression" (smaller representation). For real compression: gzip, brotli, zstd. If you want a smaller text representation of structured data, look at MessagePack, CBOR, or Protocol Buffers, but those aren't Base64.
Wrong: as a database storage format
If you're storing files in a database, store the bytes directly (BLOB, VARBINARY, bytea). Base64-encoded storage costs 33% more space, costs CPU on every read/write, and gains you nothing. Most modern databases handle binary cleanly.
The one exception: when you need a string column for some non-byte-storage reason (e.g., the storage layer is JSON-only, or you need a string primary key). Then Base64 is fine.
URL-safe vs standard
There are two flavors of Base64:
| Variant | Alphabet | Padding | Used in |
|---|---|---|---|
| Standard | A-Z a-z 0-9 + / |
= |
MIME, JSON, basic auth |
| URL-safe | A-Z a-z 0-9 - _ |
none (or optional) | JWT, OAuth tokens, URL params |
The reason: + and / are reserved characters in URLs. If you Base64-encode something and put it in a query parameter, you have to URL-encode it again (+ → %2B, / → %2F). Base64URL skips that step by using safer characters.
When you decode, make sure the decoder matches the variant. Decoding a Base64URL string with a strict standard Base64 decoder will fail or, worse, succeed with garbage data.
Common gotchas
Padding
SGVsbG8= decodes to Hello (the = is padding). Some encoders strip padding. Some decoders require it. Most decoders accept either, but not all. If you control both sides, be consistent.
Line breaks
Original RFC 2045 (MIME) Base64 wraps lines at 76 characters. Modern usage is unwrapped. If you're decoding a wrapped Base64 string and it fails, strip whitespace first:
const decoded = atob(input.replace(/\s/g, ''))
Whitespace tolerance varies
Some decoders silently skip whitespace; others error on it. Browser atob tolerates whitespace; Node's Buffer.from(s, 'base64') is more strict. When in doubt, sanitize before decoding.
Non-ASCII data in Base64
Base64 operates on bytes, not characters. To Base64 a UTF-8 string, you encode it to bytes first:
// Wrong — atob/btoa only work on Latin-1
btoa("Xin chào") // throws InvalidCharacterError
// Right, encode to UTF-8 bytes first
btoa(unescape(encodeURIComponent("Xin chào"))) // works, but ugly
// Modern way
new TextEncoder().encode("Xin chào") // → Uint8Array
Use Base64 Encode / Decode for ad-hoc conversion, it handles UTF-8 correctly.
Image-to-Base64 in practice
Inlining images as Base64 used to be popular for "performance." It's now a mixed bag:
| Image size | Inline as Base64? |
|---|---|
| < 1 KB | Yes, saves an HTTP round trip |
| 1-5 KB | Maybe, if many images on the page, they all decode CPU |
| > 5 KB | No, separate file, leverage HTTP/2 multiplexing and caching |
HTTP/2 and HTTP/3 mostly killed the "save an HTTP request" argument. Cache-busting is also harder with inlined data, change one byte, the entire CSS file has to re-download.
For tiny icons in design systems, Image to Base64 is still convenient. Just don't reach for it as a default.
Recommended workflow
- Need to send binary in JSON or URL: Base64 (URL-safe variant for URLs).
- Need to inspect a Base64 string: paste into Base64 Encode / Decode.
- Need to inspect a JWT: JWT Decoder (it's Base64URL under the hood).
- Trying to "secure" data: stop. Use encryption, not Base64.
- Trying to "compress" data: stop. Use gzip/brotli, not Base64.
The cleanest mental model: Base64 is ASCII armor. It's a way to make bytes survive a text-only journey. That's all it does, all it has ever done, and all it will ever do.
Related tools on DevTools Online:
- Base64 Encode / Decode, paste, encode or decode, in your browser
- Image to Base64, for inlining images as data URLs
- JWT Decoder. Base64URL of a token, decoded
- URL Encoder / Decoder, when Base64 needs further escaping for URLs