If you've done any web development, you've probably encountered Base64. Embedding images as strings, creating API auth headers... But why do we actually need it?
The Problem: Sending Binary Data as Text
All computer data is ultimately 0s and 1s. Images, audio files, compressed files—all binary data.
The problem? Systems like email and JSON only handle text. Put raw binary in there and things break.
The solution? Convert binary data into safe text characters. That's Base64.
How Base64 Works
The name "Base64" means it uses 64 characters.
A-Z (26) + a-z (26) + 0-9 (10) + '+' + '/' = 64
Plus = for padding.
The Conversion Process
- Convert original data to bits (0s and 1s)
- Group into 6-bit chunks (not 8-bit!)
- Map each 6-bit group to a Base64 character
- Process in 3-byte (24-bit) units. Pad with
=if needed
Converting "Hi":
H = 01001000, i = 01101001
Concatenated: 01001000 01101001
6-bit groups: 010010 | 000110 | 1001(needs padding)
→ SGk=
Real-World Use Cases
1. Data URLs (Inline Images)
Embed small images directly in HTML/CSS without HTTP requests.
<img src="data:image/png;base64,iVBORw0KGgo..." />
Pros: Fewer HTTP requests. Cons: Larger HTML, no caching
2. HTTP Basic Authentication
The username:password is Base64 encoded and sent in the header.
Authorization: Basic dXNlcjpwYXNz
(user:pass Base64 encoded)
⚠️ Warning: Base64 is NOT encryption! Anyone can decode it. Always use HTTPS.
3. Email Attachments
Email uses a text protocol (SMTP). Attachments are Base64 encoded for transmission.
4. JWT Tokens
JWT header and payload are Base64Url encoded (similar to Base64 but URL-safe characters).
The Downsides
- Size increase: ~33% larger than original (3 bytes → 4 characters)
- Not encryption: Just obfuscation, anyone can decode
- Processing overhead: Encoding/decoding uses CPU
When to Use (and When Not To)
| ✓ Good Use Cases | ✗ Bad Use Cases |
|---|---|
| Small inline images | Large file transfers (slow, inefficient) |
| Binary in JSON | Hiding sensitive data (not encryption!) |
| Text protocol compatibility | Big data (33% size increase) |
Using Base64 in JavaScript
// Encode
btoa("Hello") // "SGVsbG8="
// Decode
atob("SGVsbG8=") // "Hello"
Note: btoa/atob only support ASCII. Unicode (like emojis) requires extra handling.
A Brief History of Base64
Base64 encoding did not appear out of thin air. It evolved over decades as email systems matured. The earliest formal specification was RFC 989 (1987), which defined Privacy Enhanced Mail (PEM) and included a base64 encoding for binary data within email messages. That spec was revised through RFC 1421, then RFC 2045 (MIME), and finally RFC 4648 (2006), which is the current authoritative reference.
The reason for this long evolution is that email was originally a 7-bit ASCII system. The Simple Mail Transfer Protocol (SMTP) could only reliably transmit characters in the 0-127 range. Binary attachments, characters from non-English alphabets, and even some control characters would get corrupted in transit. Base64 solved this by mapping arbitrary bytes to a safe 64-character alphabet that every mail relay could handle without modification.
RFC 4648 formalized not just Base64 but also Base32 and Base16 (hex), giving each a clear specification. It also defined the "URL-safe" Base64 alphabet, which replaced + with - and / with _ to avoid conflicts in URLs and filenames.
Base64 vs Other Encodings
Base64 is not the only way to represent binary data as text. Each alternative has trade-offs in size, readability, and compatibility.
Hexadecimal (Base16)
Hex encoding uses 16 characters (0-9, A-F). Each byte becomes two hex characters, meaning the encoded output is exactly twice the size of the input. That is a 100% size increase, compared to Base64's 33%. Hex is common in debugging, hash representations, and low-level programming because it maps directly to nibbles (4-bit groups), making it easy to read binary values.
"Hello" in hex: 48656c6c6f (10 characters) "Hello" in Base64: SGVsbG8= (8 characters) "Hello" raw: 5 bytes
URL Encoding (Percent Encoding)
URL encoding replaces unsafe characters with a percent sign followed by two hex digits. For example, a space becomes %20. Unlike Base64, URL encoding only transforms characters that are unsafe in a URL context. ASCII letters, digits, and a few symbols pass through unchanged. This makes it efficient for text that is mostly ASCII but contains a few special characters, and wildly inefficient for binary data (each byte becomes three characters in the worst case).
ASCII85 (Base85)
ASCII85 uses 85 printable ASCII characters to encode data, achieving roughly a 25% size increase (compared to Base64's 33%). It encodes four bytes into five characters. You will find it in Adobe PostScript and PDF files. It is more space-efficient than Base64 but less widely supported in web contexts. Unless you are working with PostScript or a system that specifically requires it, Base64 is the safer choice.
Base32
Base32 uses 32 characters (A-Z and 2-7). It produces output that is 60% larger than the input, less efficient than Base64. But it has a key advantage: the output is case-insensitive. This makes Base32 ideal for situations where case might be lost, such as DNS records, file systems on case-insensitive operating systems, or one-time passwords (TOTP/HOTP apps like Google Authenticator use Base32-encoded secrets).
Base58
Base58 was invented by Satoshi Nakamoto for Bitcoin addresses. It uses 58 characters, removing visually ambiguous ones: no zero (0), no uppercase O, no lowercase l, and no uppercase I. This reduces human transcription errors when typing or reading addresses. Base58 also avoids + and /, making the output URL-safe without modification. Ripple and IPFS also use Base58-based encodings.
Encoding Comparison Table
| Encoding | Characters | Size Overhead | Common Use |
|---|---|---|---|
| Hex (Base16) | 16 | +100% | Hashes, debugging |
| Base32 | 32 | +60% | TOTP secrets, DNS |
| Base58 | 58 | +37% | Bitcoin, IPFS |
| Base64 | 64 | +33% | Email, Data URLs, JWT |
| ASCII85 | 85 | +25% | PDF, PostScript |
Performance Considerations
Base64 encoding and decoding are fast operations. On modern hardware, a well-optimized implementation can process several gigabytes per second. The bottleneck is rarely the encoding itself but rather the 33% size increase and its downstream effects.
Consider a 1 MB image. Base64-encoded, it becomes roughly 1.37 MB of text. If you embed that in an HTML page, the browser must download the extra 370 KB, parse it as text, then decode it back to binary before rendering. A separate image file, on the other hand, can be cached independently, served from a CDN, and loaded in parallel with other resources. For images larger than a few kilobytes, separate files almost always win on performance.
In API contexts, sending large binary payloads as Base64 inside JSON is common but not always ideal. If the payload is large (several megabytes or more), consider multipart form uploads instead. The server avoids the decoding overhead and the network transfers fewer bytes.
Security Misconceptions
This cannot be stated enough: Base64 is not encryption, not obfuscation, not protection. It is a transport encoding. Every Base64 string can be decoded instantly by anyone with access to it. There is no key, no secret, no security whatsoever.
Yet developers regularly make mistakes here. Storing API keys as Base64 in source code does not hide them. Sending passwords as Base64 in HTTP headers (as in Basic Auth) does not protect them. Using Base64-encoded tokens without HTTPS transmits credentials in plaintext.
If you need to protect data in transit, use TLS (HTTPS). If you need to protect data at rest, use proper encryption (AES-256-GCM, for example). Base64 is a tool for encoding, not for securing.
Handling Unicode in Base64
The JavaScript functions btoa() and atob() only work with characters in the Latin-1 range (0-255). Passing a string with characters outside that range, such as emoji or CJK characters, throws an error. The standard workaround is to encode the string as UTF-8 first, then Base64-encode the resulting bytes.
// Encoding Unicode to Base64
function utoa(str) {
return btoa(encodeURIComponent(str).replace(
/%([0-9A-F]{2})/g,
(_, p1) => String.fromCharCode('0x' + p1)
));
}
// Decoding Base64 to Unicode
function atou(b64) {
return decodeURIComponent(
atob(b64).split('').map(
c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
).join('')
);
}
utoa("Hello World!") // "SGVsbG8gV29ybGQh"
atou("SGVsbG8gV29ybGQh") // "Hello World!"
In Node.js, the Buffer class handles this more cleanly: Buffer.from(str, 'utf8').toString('base64'). Python's base64 module works with bytes objects, so you encode the string to UTF-8 first: base64.b64encode(text.encode('utf-8')).
Base64 in Modern Web Development
Despite being a decades-old standard, Base64 remains deeply embedded in modern web workflows. CSS background images can be inlined as data URIs for critical above-the-fold icons. Source maps in bundled JavaScript files often contain Base64-encoded original source code. Web fonts can be embedded inline to avoid FOIT (flash of invisible text). And practically every authentication system on the web uses Base64 somewhere in its token chain.
The key takeaway is knowing when Base64 adds value and when it adds waste. Small assets that block rendering benefit from inlining. Large assets, cacheable resources, and anything that changes independently from the HTML document should be kept as separate files. Understanding the encoding's mechanics and trade-offs lets you make that call with confidence.