Encoding
Base64 in Production — When It Helps and When It Hurts
Base64 is a transport encoding, not compression and not encryption. When it earns its 33% overhead in real systems, and when it is a code smell to rip out.
Base64 pays for itself in a handful of specific situations and is a code smell everywhere else. Engineers reach for it out of habit when they hit anything binary, then pay the 33% size penalty and the readability tax forever. This guide is the decision tree.
What Base64 actually is
Base64 maps every 3 bytes of input (24 bits) to 4 output characters (6 bits each) drawn from a 64-character alphabet: A-Z, a-z, 0-9, and two symbols. Standard Base64 uses + and /; URL-safe Base64 (RFC 4648 §5) uses - and _. Padding with = brings the output length to a multiple of 4.
That is the whole spec. It is not compression (output is 4/3 the input size, a +33% overhead before you count padding). It is not encryption (the decode is public). It is a transport encoding: a way to move bytes through a channel that only safely carries text.
| Variant | Alphabet | Padding | Used in |
|---|---|---|---|
| Standard Base64 (RFC 4648 §4) | A-Z a-z 0-9 + / | = | MIME, PEM, most APIs |
| URL-safe (RFC 4648 §5) | A-Z a-z 0-9 - _ | often omitted | JWT, URL tokens |
| Base64url no padding | same as §5 | none | JWT base64url(payload) |
The URL-safe flavor exists because + and / have meaning in URLs (+ is a space in form encoding, / is a path separator). See the URL encoding guide for the rules that make this matter.
The cost line
Before adopting Base64 anywhere, price it:
- Size:
ceil(n / 3) * 4bytes forninput bytes. A 1 MB file becomes 1.37 MB on the wire. Gzip recovers most of it for text-like payloads, but you are still burning CPU at both ends. - CPU: Negligible on modern hardware for small blobs. Real for megabyte-per-request throughput.
- Readability: A log line with 4 KB of Base64 is a log line you cannot grep.
// Quick sanity check for overhead
const bytes = new Uint8Array(1_000_000);
const b64 = btoa(String.fromCharCode(...bytes));
console.log(b64.length); // 1_333_336 — exactly 4/3 + padding
To experiment with the encode and decode steps directly, the text-to-binary and binary-to-text tools let you see the byte layout behind the scenes.
Where Base64 earns its keep
Four cases where Base64 is correct:
- Inlining small binary assets into text documents. CSS
url(data:image/png;base64,...)for favicons, email signatures, PDFs with embedded images. Under 2-4 KB the extra HTTP round-trip beats the 33% overhead. - JSON or XML payloads that must carry bytes. JSON has no binary type. Base64 in a string field is the canonical escape hatch. Alternatives (multipart, separate upload URL) are better when payloads grow past a few kilobytes.
- PEM-wrapped cryptographic keys and certificates.
-----BEGIN CERTIFICATE-----followed by Base64 of DER bytes is the universal format for X.509 and PKCS material. - JWT payloads. Header, claims, and signature are each
base64url-encoded without padding. That is not negotiable: the spec is written that way.
Where Base64 hurts
- Storing binary in a relational database as a TEXT column. Your database already has
BYTEA,BLOB,VARBINARY. Using them saves 25%, preserves indexability of hashes, and avoids an encode/decode round on every read. - Base64 in URLs of unbounded size. Browsers cap URLs around 2000-8000 chars depending on the stack. Base64 a 10 KB blob into a query string and you will hit it.
- “Obfuscating” secrets in a config file. Base64 is not a secret. Anyone can decode
echo ZHJvd3NzYXA= | base64 -d. If you need a secret, use a secret manager. - Serializing UUIDs for URLs. A UUID is already 36 hex characters. Base64url trims it to 22 but the readability loss almost never justifies the saving. See the UUID v4 vs v7 piece for the primary-key discussion.
Data URIs — the one detail that catches teams
Data URIs embed Base64 (or plain percent-encoded) bytes inside a URL: data:[<mediatype>][;base64],<data>. Three rules:
- Chrome and Firefox no longer allow data URIs for top-level navigation; they were a common phishing vector. They remain fine for
<img>, CSSurl(), and<iframe>with the caveats below. - Content Security Policy blocks
data:sources by default. You needimg-src data:or similar to re-enable. - Mobile browsers often cap data URI length around 2 MB, but performance falls off a cliff much earlier.
<!-- Good: tiny inline icon, under 1 KB -->
<img src="data:image/svg+xml;base64,PHN2Zy4uLg==" alt="">
<!-- Bad: inlining a 500 KB hero image -->
<img src="data:image/png;base64,iVBORw0KGgo...">
URL-safe vs standard — the bug you will eventually hit
A subtle trap: a library that defaults to standard Base64 produces output with + and /, which then arrives on a server that expects URL-safe Base64. Two classic failure modes:
- The
+character arrives decoded as a space because the server randecodeURIComponenton the query string before Base64-decoding. - Padding
=gets percent-encoded to%3D, the decoder does not strip it, and length validation fails.
Fix: pick one variant per interface and document it. For anything that touches URLs, JWTs, or cookies, use URL-safe without padding and normalize on receive. For MIME and PEM, stay on standard.
Performance notes
A rough mental model for when encode/decode cost matters in practice:
- Under 10 KB: effectively free on any CPU from the last decade. Do not optimize.
- 10 KB to 1 MB: microseconds to low milliseconds per operation. Still negligible for most web requests.
- 1 MB to 100 MB: now you can measure it. A 100 MB payload encoded in Node’s
Buffer.from(...).toString("base64")costs tens of milliseconds. If you are doing this in a hot path, consider streaming or avoiding the encode entirely. - Above 100 MB: Base64 is almost always the wrong answer. Use a signed URL, a multipart upload, or a side channel.
One more sharp edge worth calling out: atob and btoa in browsers work on binary strings (one-char-per-byte), not on Uint8Array. Calling btoa("é") throws InvalidCharacterError because é is two bytes in UTF-8 but btoa expects Latin-1 semantics. The correct modern approach is Uint8Array.prototype.toBase64() (Baseline 2024) or, as a fallback, encoding through a TextEncoder first.
Takeaways
Use Base64 when you are stuffing bytes through a text-only channel (JSON, URLs, PEM, email bodies). Do not use it as a database type, as obfuscation, or as a general-purpose “binary-to-string” hammer. For the encoding cousins that come up in the same conversations, see the character encoding guide and the URL encoding rules.