Base64 encoding shows up everywhere: in image data URIs, email attachments, JSON Web Tokens, HTTP Basic authentication and PEM certificates. It solves one specific problem — carrying arbitrary binary data through systems that only handle text — and it is often misunderstood. This guide explains how Base64 works bit by bit, the variants you will meet and the pitfalls to avoid.
What problem Base64 encoding solves
Many protocols and formats were designed for text. Email bodies, JSON strings, XML documents, URLs and HTTP headers can mangle or reject raw bytes such as null characters, line breaks or values above 127. Base64, defined in RFC 4648, maps any sequence of bytes onto 64 characters that are safe almost everywhere: A–Z, a–z, 0–9, + and /, with = used for padding.
How Base64 works, step by step
Base64 reads the input three bytes (24 bits) at a time and splits those bits into four groups of six. Each 6-bit number, from 0 to 63, selects one character from the alphabet. Take the text Man:
- ASCII bytes:
M= 77,a= 97,n= 110. - In binary:
01001101 01100001 01101110. - Regrouped into 6 bits:
010011 010110 000101 101110. - As numbers: 19, 22, 5, 46.
- Looked up in the alphabet (0 = A … 25 = Z, 26 = a … 51 = z, 52 = 0 … 61 = 9, 62 = +, 63 = /):
T W F u.
So Man encodes to TWFu. You can check the result with the Base64 encoder and reverse it with the Base64 decoder.
Padding
When the input length is not a multiple of three, the final group is short. The encoder fills the missing bits with zeros and adds = so the output length stays a multiple of four:
Man→TWFu(no padding)Ma→TWE=(two bytes, one=)M→TQ==(one byte, two=)
Some decoders accept missing padding and others do not, which is a common cause of “invalid Base64” errors when data moves between languages.
Size overhead
Four output characters for every three input bytes means Base64 data is about 33% larger than the original. MIME email additionally wraps lines at 76 characters, adding line breaks. That overhead is why Base64 is a poor choice for large files.
Base64URL: the URL-safe variant
Standard Base64 uses + and /, which have special meaning in URLs, and =, which must be escaped in query strings. Base64URL (also in RFC 4648) replaces + with - and / with _, and usually drops the padding. It is used in JSON Web Tokens, OAuth PKCE code challenges and anywhere tokens appear in URLs or file names. If you decode a JWT segment with a standard decoder and get errors, this is usually why — the JWT decoder handles the variant for you.
Where you will meet Base64 encoding
- Data URIs:
data:image/png;base64,iVBORw0KGgo…embeds a small image directly in HTML or CSS. Create one with image to Base64, or turn a data URI back into a file with Base64 to image. - Email attachments: MIME encodes binary attachments as Base64 text.
- HTTP Basic authentication: the header
Authorization: Basic dXNlcjpwYXNzis simplyuser:passencoded. - JSON and XML payloads: APIs send file contents or binary hashes as Base64 strings, because JSON has no binary type.
- Certificates and keys: PEM files are Base64-encoded DER data between
-----BEGIN …-----lines.
Base64 is not encryption
Base64 has no key and no secret. Anyone who sees dXNlcjpwYXNz can decode it to user:pass in a second. That is fine for transport but dangerous if it is mistaken for protection: Basic authentication is only safe over HTTPS, and storing passwords or API keys “encoded” in Base64 is the same as storing them in plain text. Use proper encryption for confidentiality and a password hashing algorithm such as bcrypt or Argon2 for passwords.
Encoding and decoding in code
JavaScript and Unicode
Browsers provide btoa() and atob(), but they work on “binary strings” where each character is one byte. btoa('✓') throws an InvalidCharacterError because the check mark is outside Latin-1. Encode the text to UTF-8 bytes first:
function toBase64(text) {
const bytes = new TextEncoder().encode(text);
let binary = '';
bytes.forEach((b) => { binary += String.fromCharCode(b); });
return btoa(binary);
}
function fromBase64(b64) {
const binary = atob(b64);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
In Node.js, use Buffer.from(text, 'utf8').toString('base64') and Buffer.from(b64, 'base64').toString('utf8'); 'base64url' is also supported as an encoding name.
Python
import base64
base64.b64encode("hello".encode()).decode() # 'aGVsbG8='
base64.b64decode("aGVsbG8=").decode() # 'hello'
base64.urlsafe_b64encode(b"\xfb\xff").decode() # '-_8=' (standard: '+/8=')
Command line
echo -n 'hello' | base64 # aGVsbG8=
echo 'aGVsbG8=' | base64 --decode
Note the -n: without it echo adds a newline, and you get aGVsbG8K — the encoding of hello\n. Unexpected trailing characters like this are one of the most common reasons two “identical” Base64 strings differ.
Fixing common Base64 decoding errors
“Invalid character”, “Incorrect padding” and garbled output account for most Base64 problems. They almost always come from one of these causes:
- The wrong alphabet. Base64URL input contains
-and_, which a standard decoder rejects. Replace them with+and/, or use a URL-safe decoder. - Missing padding. Base64URL usually omits
=. Add=characters until the length is a multiple of four before using a strict decoder. - Extra characters. Line breaks from MIME wrapping, spaces introduced when copying from a PDF or chat message, or a
data:image/png;base64,prefix left in front of the data. Strip everything outside the alphabet first. - Plus signs turned into spaces. If Base64 travelled through a URL query string without percent-encoding, each
+may arrive as a space. Encode the value before putting it in a URL, or use Base64URL. - The wrong character set. Decoding succeeds but accented letters look like
é: the bytes were UTF-8 but were interpreted as Latin-1 (or the reverse). Decode to bytes, then decode the bytes with the character set that was used to encode them.
A quick sanity check helps: valid Base64 has a length that is a multiple of four (with padding), contains only the 64 alphabet characters plus trailing =, and never has = in the middle. If a decoder produces binary noise instead of the text you expected, the input may be compressed or encrypted data that was Base64-encoded afterwards — decoding is only the first step.
When not to use Base64
- Large images and files: a data URI cannot be cached separately and is a third larger. Serve files normally and inline only tiny icons.
- URLs: prefer Base64URL, or percent-encoding with URL encode when the data is already text.
- Security: never as a substitute for encryption or hashing.