When You Should NOT Use Base64 Encoding
Base64 is the duct tape of the web — and like real duct tape, it's used in places it absolutely shouldn't be. The five anti-patterns and what to do instead.
Base64 has a job: make binary data survive a text-only transport. When that’s the actual problem, use it. When it isn’t, the costs (33% size growth, slower processing, false sense of security, debugging headaches) make it the wrong choice.
This post is the five places I see Base64 misused most — what to do instead, and why.
1. As “encryption” or to “hide” sensitive data
// Don't do this
const "encrypted_password" = btoa(userPassword);
The problem: Base64 is a reversible encoding with a publicly documented alphabet. Anyone who sees the encoded form can decode it in milliseconds. Encoded output is not secret.
I have seen this in:
- Configuration files where someone Base64-encoded an API key thinking it was protected
- localStorage where a “secret” token was Base64’d before saving
- HTTP headers where a custom auth scheme used
Base64(user:pass)thinking it was secure
That last one is literally HTTP Basic Auth, which is documented as insecure on its own and only safe over HTTPS. Even then, it’s plaintext credentials any proxy can read.
What to do instead:
- For passwords at rest: hash with bcrypt, argon2id, or scrypt. Never store reversibly. (See why MD5 is dead for context on hash choice.)
- For secrets in transit: TLS. Then add HMAC signatures or proper auth tokens (JWT, OAuth) on top.
- For secrets at rest in client storage: don’t. Browser storage is not a vault. Use OAuth flows that issue short-lived tokens server-side.
- For actual encryption: AES-GCM via WebCrypto, or a vetted library like libsodium. Never roll your own.
If your security relies on the attacker not knowing about Base64, your security is zero.
2. To shrink data
// What people sometimes try
const compressed = btoa(longText);
The problem: Base64 makes data 33% larger, not smaller. The 4-characters-for-3-bytes ratio is unavoidable.
Where this confusion comes from: people see Base64’d data look “compact” because it’s all printable characters with no spaces, and assume that’s compression. It’s not — it’s just denser-looking than the JSON-with-newlines they were comparing to.
What to do instead:
- For size-sensitive transport: gzip/brotli on HTTP. Both are universally supported, both shrink text 60-80%.
- For binary fields in JSON: use a binary-aware transport (Protocol Buffers, MessagePack, CBOR). They serialize binary as bytes, not Base64.
- For inline images in CSS/HTML: only worthwhile under ~2KB. Beyond that, the per-cache-hit 33% penalty is worse than one HTTP request.
If you really need bytes inside a text format, you’re stuck with Base64. But don’t kid yourself that it’s saving anything.
3. To make data “URL-safe” without thinking
// Sometimes seen
const url = `/api/data?payload=${btoa(JSON.stringify(obj))}`;
The problem: Standard Base64 includes + and /, both of which are special in URLs. The + may be decoded as space; the / confuses URL parsers. Also, = padding sometimes needs URL-encoding to %3D. So you’ve turned one encoding problem into three.
What to do instead:
- If you need to put binary in a URL: use URL-safe Base64 (RFC 4648 §5) —
-and_instead of+and/, with padding stripped. The Base64 tool has this as a toggle. - For structured data in URL query strings: use
URLSearchParams(encodeURIComponentunder the hood). For complex nested data, send it in a POST body or use a hash-route pattern. - For transient state in a URL: server-side, generate a short opaque ID and look up the data. Don’t put the data itself in the URL.
If your URL is so long you need Base64 to compress it, your URL is too long. Refactor the API.
4. To bypass content-type rules
<img src="data:image/png;base64,iVBORw0KGgo..." alt="" />
This use is fine for small images, when you actually need them inline. Below ~2KB, removing the HTTP roundtrip is worth the 33% size penalty plus the lost browser cache (data URLs aren’t cacheable separately from the page they’re on).
The problem starts when:
- People inline 50KB images this way “to save a request” and watch their page weight balloon
- Inlined images can’t be cached across pages — every page load re-downloads them
- Build tools sometimes Base64-inline by default, and the result is HTML files larger than the assets they replaced
What to do instead:
- Small icons (< 2KB): inline as data URL is fine, or use SVG inline as text (which is far smaller than Base64’d raster).
- Logos, hero images: load normally. HTTP/2 multiplexes them; the request cost is negligible.
- Vector graphics: ship SVG, not Base64’d PNG.
The right test: if removing the data URL and serving the asset normally makes the page faster (it usually does for anything > 2KB), don’t use Base64.
5. As a checksum or content fingerprint
I’ve seen this pattern in two places. Both wrong.
// "Verifying" content matched
if (btoa(receivedFile) === btoa(expectedFile)) { /* ... */ }
// Cache key generation
const cacheKey = btoa(url + params);
The problem:
- Base64 is not a checksum. It doesn’t detect modification —
Base64(data)is bit-by-bit identical to itself, but two functionally identical files could have different Base64 encodings (e.g., one has trailing newline, one doesn’t). It compares no better than the original bytes. - Base64 is not a hash. There’s no compression to a fixed-length digest. The output grows linearly with input, so it’s a terrible cache key.
What to do instead:
- For content equality: SHA-256 or BLAKE3 hash and compare digests. Fixed-length, collision-resistant.
- For cache keys: SHA-256 or MD5 (cryptographically broken but acceptable for caching) of the canonical form. The fixed-length digest gives you constant-time key lookup.
- For file integrity check on download: ship a
.sha256sidecar file. Compare locally.
The Hash Generator computes all of these, client-side, with files up to 100MB.
When Base64 is the right answer
To balance the screed:
- JWTs: tokens have to be header-safe and URL-safe text. Base64 (URL-safe) is the right tool.
- Email MIME: SMTP was 7-bit; Base64 is how attachments survive. Still relevant for legacy systems.
- Inline data in CSS for small assets: see above — under 2KB, fine.
- Wrapping binary signatures in JSON fields: when your protocol mandates JSON and you need to ship a hash or signature, Base64 it into a string field.
- Embedding files in version control: e.g., Git LFS pointers, mac plists with binary blobs.
In each of these, the actual problem is “binary data needs to survive a text-only channel.” That’s exactly what Base64 was designed for.
A working principle
When you reach for Base64, ask: what text-only channel am I forcing binary through? If you can’t name one — if there’s no JSON field, no URL, no email body, no console log — you’re using Base64 because you saw it somewhere. Don’t.
If the channel exists, then go ahead. Use it. Use URL-safe variants when the channel is a URL. Use the proper UTF-8 path for international text. And always remember: it’s encoding, not encryption.
Further reading
Related posts
- Base64: How It Actually Works Under the Hood — Base64 is everywhere — in JWTs, data URLs, email attachments. This is the byte-l…
- Percent Encoding and RFC 3986 Explained — Why is `+` sometimes a space and sometimes a literal plus? Why does `%2520` show…
Related tool
Encode and decode Base64 strings and files. Client-side, safe for sensitive data.
Written by Mian Ali Khalid. Part of the Encoding & Crypto pillar.