What Base64 Actually Does (and What It Definitely Does Not)
Base64 turns arbitrary bytes into 64 safe characters, costs you a third of your payload size, and offers exactly zero security. It is one of the most useful tools in the protocol toolbox and one of the most consistently misunderstood — usually by someone who thinks it counts as obfuscation.
The short version
- Base64 is an encoding, not encryption. Anyone can reverse it instantly, with no key.
- It costs 33% more bytes — every 3 bytes of input become 4 characters of output.
- Standard Base64 uses
+and/, which are unsafe in URLs. Use base64url, which substitutes-and_. - JavaScript's
btoa()throws on any character above U+00FF. Encode to UTF-8 bytes first.
The problem it solves
Plenty of systems are not binary-safe. SMTP was specified for 7-bit ASCII. JSON strings cannot hold arbitrary bytes — a null byte or a lone surrogate is not valid. XML has forbidden control characters. HTTP headers are text. Yet we constantly need to move binary data — an image, a signature, an encrypted blob — through these text-only channels.
Base64 (RFC 4648) solves it by re-expressing bytes in an alphabet that every text system tolerates: A–Z, a–z, 0–9, plus + and /. Sixty-four characters means each one carries exactly 6 bits.
How the transformation works
Take 3 bytes — 24 bits — and regroup them into four 6-bit chunks. Each chunk indexes the alphabet:
Input: M a n
Bytes: 77 97 110
Binary: 01001101 01100001 01101110
Regroup: 010011 010110 000101 101110
Index: 19 22 5 46
Output: T W F u → "TWFu"
Three bytes in, four characters out. That ratio is the whole story: 4/3 means 33.3% size growth, always. A 3 MB image becomes a 4 MB data URI. This is why inlining large assets as Base64 is usually the wrong optimisation — you trade one HTTP request for a third more bytes on every page load, and the encoded form does not compress as well as the original binary.
Padding, and why = appears
When the input length is not a multiple of 3, the final group is short and the output is padded to a multiple of 4 with =:
| Input bytes | Leftover | Output chars | Padding |
|---|---|---|---|
| 3, 6, 9… | 0 | 4, 8, 12… | none |
| 4, 7, 10… | 1 | 2 + padding | == |
| 5, 8, 11… | 2 | 3 + padding | = |
So "M" encodes to "TQ==" and "Ma" to "TWE=". The padding carries no data — it exists so a decoder consuming a stream in 4-character blocks knows where the input ended. Some contexts drop it: JWTs omit padding entirely, as does base64url by convention. A decoder that rejects unpadded input will fail on perfectly valid JWT segments, which is a recurring source of "invalid token" bugs in hand-rolled parsers.
Base64url: the variant for URLs and filenames
Two characters in the standard alphabet cause trouble outside a plain text body:
+— in a query string, form-decoding turns this into a space, silently corrupting the data./— in a path, this is a segment separator, so your single value becomes two path components.=— legal but awkward; it is the key/value separator in query strings and often gets escaped to%3D.
RFC 4648 §5 defines base64url, which is identical except that index 62 is - instead of + and index 63 is _ instead of /. Padding is usually omitted. The conversion is a two-character substitution:
const toUrlSafe = b64 => b64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
This is exactly what JWTs use for all three segments, which is why you see - and _ in tokens but rarely + or /. If you are putting Base64 in a URL, a filename, or a cookie value, use base64url. If you are putting it in a JSON body or an email, standard Base64 is fine.
It has no key, no secret and no variability — the same input always produces the same output, and every decoder on earth will reverse it. HTTP Basic authentication transmits base64(user:password), which is why Basic auth over plain HTTP is equivalent to sending the password in clear text. Base64-encoding a value in a cookie, a config file, or a mobile app's source does not protect it; it only makes it marginally less obvious to a casual reader. If you need confidentiality, you need encryption.
Encoding Unicode in JavaScript
The browser's btoa() is older than modern JavaScript string semantics, and it has a sharp edge: it operates on "binary strings", where each character must be in the range U+0000–U+00FF. Anything else throws:
btoa("héllo") // InvalidCharacterError
btoa("日本語") // InvalidCharacterError
btoa("🎉") // InvalidCharacterError
The fix is to convert the string to UTF-8 bytes first, then map those bytes into the Latin-1 range that btoa expects:
function encodeUtf8Base64(str) {
const bytes = new TextEncoder().encode(str); // UTF-8 bytes
let binary = '';
bytes.forEach(b => { binary += String.fromCharCode(b); });
return btoa(binary);
}
function decodeUtf8Base64(b64) {
const binary = atob(b64);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes); // back to a string
}
The conceptual point is worth holding onto: Base64 encodes bytes, not text. To encode a string you must first decide how that string becomes bytes, and that decision is a character encoding — almost always UTF-8. Skipping that step is what produces mojibake after a round trip.
Encode and decode Base64 in your browser
Layerbit's Base64 tool handles text and file input, runs entirely client-side, and never uploads what you paste into it — which matters when the thing you are decoding is a token or a credential from a log file.
Where you will actually meet it
| Context | Variant | Note |
|---|---|---|
| JWT segments | base64url, unpadded | Header and payload are plainly readable |
| Data URIs | Standard | data:image/png;base64,… |
| HTTP Basic auth | Standard | Credentials in clear text for practical purposes |
| MIME email | Standard, 76-char lines | Line breaks push overhead to roughly 37% |
| PEM keys and certificates | Standard, 64-char lines | Between the -----BEGIN----- markers |
| Kubernetes Secrets | Standard | Encoded, not encrypted — enable encryption at rest separately |
That last row is a common misunderstanding with real consequences. A Kubernetes Secret stores values Base64-encoded, which makes them look protected in a manifest. They are not. Anyone with read access to the Secret can decode it in one command, and by default they are stored unencrypted in etcd.
Related encodings you may want instead
- Hex (base16) — 100% overhead, but trivially readable and impossible to get the alphabet wrong. Good for short values like hashes and checksums.
- Base32 — 60% overhead, case-insensitive, no ambiguous characters. Used for TOTP secrets and onion addresses precisely because a human may need to type it.
- Base85 / Ascii85 — only 25% overhead, but uses characters that need escaping in most contexts. Mostly seen inside PDFs.
Base64 sits at a deliberate sweet spot: low enough overhead to be practical, an alphabet safe enough for almost every text channel. That balance is why it has outlived nearly every alternative.
A transport format, nothing more
Base64 has exactly one job: get bytes safely through a channel that only accepts text. It does that job well, at a predictable 33% cost, and it does nothing else. It does not compress, does not protect, and does not validate.
So when you see it, ask what it is carrying and whether that thing needs protecting on its own terms. A Base64 string in a URL is fine. A Base64 password in a config file is a password in a config file — the encoding changed nothing except how long it takes a reader to notice.