What is Base64 encoding and when should you use it?
· 6 min read
Base64 shows up everywhere once you start looking: in Authorization: Basic headers, in JWTs, in data URIs, in email attachments, in Kubernetes secrets. It’s also one of the most misunderstood encodings. People treat it as a way to “hide” data, run into errors with non-English text, and wonder why a 3 MB image becomes a 4 MB string.
This article explains how Base64 works bit by bit, which variant to use where, and the traps in JavaScript and elsewhere. To see the examples live, use the Base64 encoder and decoder, which handles UTF-8 and URL-safe input correctly.
The problem Base64 solves
Many systems were built to carry text, not arbitrary bytes. Email (SMTP) was specified for 7-bit ASCII, URLs have reserved characters, and JSON strings can’t hold raw binary. If you need to put an image, a key or a compressed blob through one of these channels, you need a way to write any sequence of bytes using only “safe” characters.
Base64 does that with 64 characters that survive nearly every text system:
A–Z a–z 0–9 + / (and = for padding)
How it works, bit by bit
Base64 takes the input 3 bytes (24 bits) at a time and splits them into four groups of 6 bits. Each 6-bit group is a number from 0 to 63, which picks one character from the alphabet.
The textbook example is the word Man:
| Step | Value |
|---|---|
| Text | M a n |
| ASCII codes | 77, 97, 110 |
| Bits | 01001101 01100001 01101110 |
| Regrouped by 6 bits | 010011 010110 000101 101110 |
| Values | 19, 22, 5, 46 |
| Base64 | T W F u |
So Man encodes to TWFu. When the input length isn’t a multiple of 3, the last group is padded with zero bits and the output with =:
| Input | Output |
|---|---|
Man |
TWFu |
Ma |
TWE= |
M |
TQ== |
hello |
aGVsbG8= |
That’s the whole algorithm. You can check each row in the Base64 tool.
The size cost: always about 33%
Every 3 bytes become 4 characters, so Base64 output is 4/3 of the input size, rounded up to a multiple of 4. A 3 MB file becomes 4 MB of text. MIME email also adds a line break every 76 characters, which adds a little more.
That overhead is the main reason to avoid Base64 when you don’t need it. Uploading a file as Base64 inside JSON costs a third more bandwidth and memory than sending the raw bytes as multipart/form-data or a binary body.
Standard vs URL-safe Base64
Two characters of the standard alphabet, + and /, have special meanings in URLs, and = is used in query strings. RFC 4648 defines a second alphabet for URLs and file names:
| Variant | Characters 62 and 63 | Padding | Where you see it |
|---|---|---|---|
| Standard | + / |
= required |
MIME email, PEM keys, data URIs |
| URL-safe (base64url) | - _ |
Usually omitted | JWTs, URL tokens, file names |
The same bytes produce different strings:
standard: SGk/Pg==
base64url: SGk_Pg
Decoders are often strict about which variant they accept. A JWT is always base64url without padding, so feeding one of its parts to a standard decoder can fail on - or _, or on the missing padding. The Base64 decoder here detects the variant and adds missing padding itself, which saves a round of trial and error. The JWT debugger decodes token parts the same way.
The btoa trap in JavaScript
Browsers have had btoa and atob since forever. They don’t encode text; they encode “binary strings”, where each character must have a code point below 256. So this works:
btoa('hello'); // "aGVsbG8="
btoa('é'); // "6Q==" (Latin-1 byte 0xE9, not UTF-8)
And this throws:
btoa('Привет');
// InvalidCharacterError: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.
Even when it doesn’t throw, btoa('é') produces the Latin-1 encoding, which a server expecting UTF-8 decodes as garbage. The correct way is to encode text to UTF-8 bytes first:
const bytes = new TextEncoder().encode('Привет');
const b64 = btoa(String.fromCharCode(...bytes)); // "0J/RgNC40LLQtdGC"
// and back
const text = new TextDecoder().decode(Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)));
(For large inputs, build the binary string in chunks: spreading a multi-megabyte array into String.fromCharCode hits the argument limit.) Newer runtimes also have Uint8Array.prototype.toBase64() and Uint8Array.fromBase64(), which support both alphabets directly. In Node.js, Buffer.from(text).toString('base64') has always handled UTF-8.
Base64 is not encryption
This deserves its own section because it keeps causing incidents. Base64 has no key. Anyone can decode it, instantly, with a tool that ships in every browser. Some examples of what that means in practice:
Authorization: Basic dXNlcjpwYXNzd29yZA==is the stringuser:password. Basic auth is only safe over HTTPS.- Kubernetes
Secretvalues are Base64-encoded, not encrypted. Anyone who can read the object can read the secret, unless encryption at rest and RBAC are configured. - The payload of a JWT is readable by anyone who has the token. Its signature proves who issued it, but it hides nothing.
If data must be secret, encrypt it, and treat the Base64 as a transport wrapper around the ciphertext.
When to use Base64 and when not to
Good uses:
- Embedding small binary data in text formats: a tiny icon in CSS, a certificate in a PEM file, a key in an environment variable.
- Carrying bytes through a text-only channel: email attachments, JSON APIs that must stay JSON, tokens in URLs (base64url).
Poor uses:
- Large files in JSON request bodies. Use multipart uploads or a direct binary upload.
- Images in HTML “for performance”. Small icons can win; photos almost never do. The image to Base64 converter shows the size cost for your own files.
- Anything described as “obfuscation” or “security”.
Quick reference
| Task | Command |
|---|---|
| Encode a file on Linux | base64 file.bin (-w0 to disable wrapping) |
| Decode on Linux | base64 -d file.b64 |
| Decode on macOS | base64 -D -i file.b64 (or -d on newer versions) |
| Node.js | Buffer.from(s).toString('base64') / 'base64url' |
| Python | base64.b64encode(data) / base64.urlsafe_b64encode(data) |
For quick checks, paste text or a file into the Base64 encoder and decoder. It encodes UTF-8 correctly, decodes both alphabets, and runs locally, so it’s fine to paste tokens and keys.