Base64 Encoding Explained — How It Works, Variants, and When to Use It
Base64 is the most widely used binary-to-text encoding scheme on the internet. It turns raw bytes into printable ASCII characters, making binary data safe for transport through text-only channels: email attachments (MIME), JSON Web Tokens, data URIs in CSS/HTML, X.509 certificates (PEM), and cryptographic key exchange. This guide explains exactly how Base64 works, the three main variants, and when to use each.
1. Why Base64 Exists
Many internet protocols — SMTP (email), HTTP headers, JSON, XML — were designed for human-readable text. Sending raw binary through these channels corrupts data because:
- Some byte values (0x00, control characters) have special meanings in text protocols.
- 8-bit bytes get mangled by systems that assume 7-bit ASCII.
- Binary data can't be embedded directly in JSON strings without escaping.
Base64 solves this by converting any binary data into a safe subset of 64 ASCII characters: A-Z, a-z, 0-9, +, /, and = for padding.
2. How the Algorithm Works
Base64 processes input in 3-byte (24-bit) groups and outputs 4 characters per group:
Input: M a n (3 bytes = 24 bits)
ASCII: 0x4D 0x61 0x6E
Binary: 01001101 01100001 01101110
Split into 4 groups of 6 bits:
010011 010110 000101 101110
Decimal: 19 22 5 46
Base64: T W F u
Result: "TWFu"
Step-by-Step Algorithm
- Group bytes: Take 3 bytes (24 bits) at a time. If fewer than 3 bytes remain, pad with zeros.
- Split into 6-bit chunks: Divide the 24 bits into four 6-bit values (0–63).
- Map to Base64 alphabet: Use the lookup table — 0→A, 1→B, ..., 25→Z, 26→a, ..., 51→z, 52→0, ..., 61→9, 62→+, 63→/.
- Add padding: If input had 1 extra byte → output 2 chars +
==. If 2 extra bytes → output 3 chars +=.
The Base64 Alphabet
| Value | Char | Value | Char | Value | Char | Value | Char |
|---|---|---|---|---|---|---|---|
| 0–25 | A–Z | 26–51 | a–z | 52–61 | 0–9 | 62 | + |
| 63 | / | (padding) = | |||||
3. Three Base64 Variants
3.1 Standard Base64 (RFC 4648 Section 4)
The original encoding. Uses + and / as characters 62 and 63, with = padding. This is what btoa() produces in JavaScript and base64.b64encode() in Python.
Input: "Hello!" Output: "SGVsbG8h"
3.2 URL-Safe Base64 (base64url, RFC 4648 Section 5)
Replaces + with - and / with _ so the output is safe in URLs, filenames, and path segments. Padding (=) is often omitted because it conflicts with URL query parameter syntax.
Used in: JWT tokens, JWS/JWE signatures, OAuth 2.0 PKCE code verifiers, FIDO2 WebAuthn challenge strings.
Standard: "a+b/c==de" URL-safe: "a-b_cde" (padding stripped) // JWT example (period-separated): eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.dYBjZo3v
3.3 MIME Base64 (RFC 2045)
For email attachments and multipart MIME messages. Wraps output at 76 characters per line using \r\n line breaks. Uses the same alphabet as Standard Base64.
Used in: Email attachments, SOAP messages with binary content, embedding images in HTML emails.
4. Padding: Why = and ==?
The = character is not part of the 64-character alphabet — it signals how many padding bytes were added:
| Input Length | Output Chars | Padding | Example |
|---|---|---|---|
| Multiple of 3 (e.g., 6 bytes) | 8 chars | None | SGVsbG8h |
| 1 extra byte (e.g., 4 bytes) | 6 chars | == | SGVsbA== |
| 2 extra bytes (e.g., 5 bytes) | 7 chars | = | SGVsbG8= |
Many Base64 decoders tolerate missing padding. URL-safe Base64 often omits it entirely since the output length is determined by the surrounding protocol (e.g., a JWT payload always has a known byte length from the JSON structure).
5. Common Use Cases
5.1 Data URIs (Embedding Binary in HTML/CSS)
Inline images, fonts, and other binary assets directly in HTML or CSS without separate HTTP requests.
<img src="data:image/png;base64,iVBORw0KGgoAAAANS..."> /* CSS background */ background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0i...);
5.2 JSON Web Tokens (JWT)
JWTs use URL-safe Base64 without padding for header, payload, and signature segments.
// JWT structure: base64url(header).base64url(payload).base64url(signature)
5.3 PEM Certificate Format
X.509 certificates, private keys, and CSRs use Base64 with specific begin/end markers and line wrapping (64 chars per line).
-----BEGIN CERTIFICATE----- MIIDXTCCAkWgAwIBAgIJAKLp5n6G7u0UMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV ... -----END CERTIFICATE-----
5.4 Email MIME Attachments
Binary attachments (PDFs, images, ZIP files) are Base64-encoded within MIME multipart messages.
Content-Type: application/pdf Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="report.pdf" JVBERi0xLjQKJeLjz9MKNCAwIG9iago8PC9UeXBlL1hSZWZ1bm...
5.5 Binary Keys in JSON
When an API needs to transmit raw cryptographic keys or binary metadata inside JSON, Base64 is the standard container.
{
"kty": "EC",
"x": "base64url-encoded-x-coordinate",
"y": "base64url-encoded-y-coordinate"
}
6. Base64 vs Hex Encoding
| Property | Base64 | Hex |
|---|---|---|
| Expansion ratio | 4:3 (33% overhead) | 2:1 (100% overhead) |
| Character set | 64 chars (A-Za-z0-9+/) | 16 chars (0-9A-F) |
| Readability | Low (case-sensitive, symbols) | Medium (fixed-width per byte) |
| Best for | Large binary: images, files, certificates | Small values: hashes, keys, APDUs |
For debugging binary protocols like APDUs or EMV card data, hex is preferred because each byte maps to exactly 2 characters. For embedding files or images, Base64 wins with 33% less overhead.
7. Try It Yourself
Use our free online tool to encode and decode Base64 in all three variants — Standard, URL-safe, and MIME:
8. Quick Reference — Code Snippets
JavaScript
// Standard Base64
const encoded = btoa("Hello, World!"); // "SGVsbG8sIFdvcmxkIQ=="
const decoded = atob(encoded);
// URL-safe Base64
function toBase64url(buf) {
return btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Handle Unicode strings (TextEncoder handles multi-byte)
function utf8ToBase64(str) {
const bytes = new TextEncoder().encode(str);
return btoa(String.fromCharCode(...bytes));
}
Python
import base64
# Standard
encoded = base64.b64encode(b"Hello, World!").decode() # "SGVsbG8sIFdvcmxkIQ=="
decoded = base64.b64decode(encoded)
# URL-safe
encoded = base64.urlsafe_b64encode(data).decode().rstrip('=')
# MIME (with line wrapping)
encoded = base64.encodebytes(data).decode()