The first version of the Base64 converter I shipped on toolz.dev had a bug I'm still slightly embarrassed about. It worked perfectly in every test I wrote β encoded, decoded, round-tripped, done. Then someone pasted text containing an emoji and got this:
Uncaught DOMException: InvalidCharacterError:
Failed to execute 'btoa' on 'Window': The string to be
encoded contains characters outside of the Latin1 range.
I'd built a text encoding tool and forgotten that text includes, you know, most text. Every non-Latin script, every accented character, every emoji β broken. My tests were all ASCII because I think in ASCII. That bug taught me more about Base64 than any spec reading did, and I'll show you the fix later because it trips up nearly everyone who touches btoa().
Base64 is one of those things developers use daily β inside every JWT, every email attachment, every data: URI β while rarely looking under the hood. Let's look under the hood.
TL;DR: Base64 encoding converts binary data into 64 safe ASCII characters so it can travel through text-only channels like JSON, URLs, and email β at the cost of ~33% size overhead. It is not encryption; anyone can reverse it instantly. To encode or decode right now, use the free client-side Base64 Converter on toolz.dev β your data never leaves the browser, which matters when you're decoding tokens.
What Is Base64 Encoding?
Base64 is a binary-to-text encoding scheme: it represents arbitrary bytes using only 64 characters that survive every text system ever built. The authoritative spec is RFC 4648 (2006), though the encoding goes back to RFC 1421 and Privacy Enhanced Mail in 1993 β Base64 is older than the web browser.
The alphabet:
AβZβ values 0β25aβzβ values 26β510β9β values 52β61+β 62,/β 63=β padding (not a value, just filler)
Why these 64? Because they survive unmangled in ASCII, EBCDIC, and every email gateway ever built. Base64 is a peace treaty with decades of text-only infrastructure.
The cost of the treaty: every 3 input bytes become 4 output characters β a fixed 33% size tax. Keep that number in your head; it decides real architecture questions.
How Does the Base64 Algorithm Work?
Shorter answer than you'd expect: regroup bits from 8s into 6s, then look them up in a table. 2βΆ = 64 β that's where the name comes from.
Encoding Hi!:
Step 1 β bytes to bits.
| Character | ASCII | Binary |
|---|---|---|
| H | 72 | 01001000 |
| i | 105 | 01101001 |
| ! | 33 | 00100001 |
Concatenated: 010010000110100100100001 β 24 bits.
Step 2 β regroup into 6-bit chunks.
010010 | 000110 | 100100 | 100001
18 | 6 | 36 | 33
Step 3 β look up each value in the alphabet.
18 β S, 6 β G, 36 β k, 33 β h. So Hi! encodes to SGkh.
That's the entire algorithm. No math beyond a lookup table.
Padding handles inputs that aren't multiples of 3 bytes. Encode just Hi (2 bytes = 16 bits) and you can only fill two-and-a-bit 6-bit groups; the encoder zero-pads the bits and appends = to signal how much is filler:
Hi β SGk= (2 bytes remaining β one '=')
H β SA== (1 byte remaining β two '==')
Hi! β SGkh (multiple of 3 β no padding)
When I implemented this for the toolz.dev converter, padding was where all my off-by-one bugs lived. If you ever hand-roll Base64 β for a coding interview, say β write the padding tests first.
Decoding is the mirror image: characters back to 6-bit values, regroup into 8-bit bytes, drop the padding.
Where Do You Actually Run Into Base64?
JWTs β the big one
Every JSON Web Token is three Base64URL-encoded segments joined by dots: header, payload, signature. Debugging auth is 50% decoding these.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
Decode the first segment and you get {"alg":"HS256","typ":"JWT"}. The second gives you the claims. My workflow when a token misbehaves: split at the dots, decode each part in the Base64 Converter, then paste the JSON into the JSON Formatter to read it properly. Two pastes, and you know whether the exp claim is your problem.
Worth saying plainly: JWT payloads are readable by anyone holding the token. The signature stops tampering, not reading. I've reviewed codebases that stuffed sensitive data into claims because the gibberish look implied privacy. It doesn't.
Data URIs
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." />
Embedding small assets inline saves an HTTP request. My rule of thumb from building image-heavy WordPress admin UIs: worth it under ~5 KB, past 10 KB you're bloating the document by 33% to save a request HTTP/2 multiplexes anyway.
Kubernetes Secrets β and a rant
apiVersion: v1
kind: Secret
data:
password: cGFzc3dvcmQ= # decodes to "password"
Kubernetes Secrets are Base64-encoded, and the false security this implies is alarming. That value decodes in one paste. Base64 here exists so binary values survive YAML β a formatting decision, not a security one. If your secrets story ends at "they're Base64 in etcd," it hasn't started.
The rest of the list
HTTP Basic Auth headers (Authorization: Basic dXNlcjpwYXNz decodes to plain user:pass β hence HTTPS-only). Email attachments via MIME. Binary blobs inside JSON payloads, because JSON has no binary type.
Is Base64 Encryption? (No. Please, No.)
Worth its own section because the misconception refuses to die.
Base64 is a representation, like writing a number in hexadecimal. No key. No secret. Decoding requires nothing but the alphabet table printed publicly in RFC 4648. Anything Base64-"protected" is protected the way a letter is protected by being written in cursive.
If data needs confidentiality: encrypt properly (AES-GCM, or libsodium), then Base64-encode the ciphertext if the channel needs text. Encoding and encryption compose fine β they just aren't substitutes. And if you need integrity rather than secrecy, that's a hash's job β the Hash Generator covers SHA-256 and friends.
How Does Base64 Compare to Hex, URL Encoding, and Base85?
| Base64 | Hex (Base16) | URL/percent encoding | ASCII85 | |
|---|---|---|---|---|
| Size overhead | +33% | +100% | 0β200%, content-dependent | +25% |
| Alphabet | 64 chars | 16 chars | ASCII + %XX escapes |
85 chars |
| Human-readable output | No | Sort of β byte boundaries visible | Mostly, for ASCII input | No |
| URL-safe by default | No (+, /, =) |
Yes | Yes, by definition | No |
| You'll meet it in | JWTs, MIME, data URIs | Hashes, MAC addresses, color codes | Query strings | PDF internals |
How I choose: hex when humans will read or compare the output β checksums, digests, anything debugged by eyeball. Percent-encoding for URL text, never binary. Base64 for binary crossing a text channel β most real cases. ASCII85 never voluntarily; the 8% saving has yet to justify the compatibility questions.
What Is URL-Safe Base64 and Why Does It Exist?
Standard Base64 has a problem: + means "space" in query strings, / is the path separator, = delimits parameters. Put a standard-Base64 token in a URL and some middleware, somewhere, will mangle it β intermittently, and only in production. Ask me how I know.
RFC 4648 section 5 defines the fix, usually called Base64URL:
| Standard | URL-safe |
|---|---|
+ |
- |
/ |
_ |
= padding |
usually just omitted |
Same algorithm, two characters swapped, padding dropped. JWTs use Base64URL exclusively β which is exactly why pasting a JWT segment into a strict standard-Base64 decoder sometimes fails on a stray - or _. The toolz.dev converter handles both variants, because a decoder that rejects half of real-world Base64 isn't much of a decoder.
Rule of thumb: if the encoded string will ever touch a URL, filename, or HTTP header, use the URL-safe variant from the start. Retrofitting is a find-and-replace plus a prayer.
How Do You Encode and Decode in Code?
JavaScript β the trap I fell into
Here's the naive version, the one I shipped:
btoa('Hello') // "SGVsbG8=" β great!
btoa('cafΓ© β') // InvalidCharacterError β the bug from my intro
btoa predates modern Unicode handling and only accepts Latin-1. The correct modern approach goes through UTF-8 bytes explicitly:
// Encode: string β UTF-8 bytes β Base64
const bytes = new TextEncoder().encode('cafΓ© β');
const encoded = btoa(String.fromCharCode(...bytes)); // "Y2Fmw6kg4piV"
// Decode: Base64 β bytes β string
const decoded = new TextDecoder().decode(
Uint8Array.from(atob(encoded), c => c.charCodeAt(0))
); // "cafΓ© β"
(In Node.js, skip the ceremony: Buffer.from(str, 'utf8').toString('base64').)
Python
import base64
encoded = base64.b64encode('cafΓ© β'.encode('utf-8')).decode('ascii')
decoded = base64.b64decode(encoded).decode('utf-8')
# URL-safe variant β note -_ instead of +/
token = base64.urlsafe_b64encode(b'binary\xfb\xff').decode('ascii')
Python makes the right thing obvious: you must pass bytes, so the encode-to-UTF-8 step can't be forgotten. I wish btoa had been designed with the same spine.
PHP
$encoded = base64_encode('cafΓ© β'); // handles bytes as-is β PHP strings ARE bytes
$decoded = base64_decode($encoded);
// URL-safe requires manual translation β a WordPress-plugin-developer classic:
$urlSafe = rtrim(strtr($encoded, '+/', '-_'), '=');
That strtr/rtrim line has appeared in every PHP codebase I've ever worked on, including WP Adminify. PHP never got a built-in URL-safe variant, so we all keep writing the same two lines.
Frequently Asked Questions
What is Base64 encoding used for?
It converts binary data into ASCII text so it can pass through systems that only handle text: JSON payloads, URLs, email (MIME), HTTP headers. You meet it most often in JWT tokens, data: URIs for inline images, Kubernetes Secrets, and API payloads carrying files. It's a transport format, not a storage or security format.
Is Base64 the same as encryption?
No, and confusing the two causes real security incidents. Base64 has no key β decoding requires only the public alphabet table, and takes one paste into any decoder. Encrypt first with a real algorithm (AES-GCM), then encode the ciphertext if the channel needs text.
Why does Base64 make data 33% larger?
Each Base64 character carries 6 bits of information but occupies a full 8-bit byte, so 3 bytes of input always become 4 characters of output β 4/3 β 1.33. It's a fixed cost of the format, unavoidable by design. If size matters, compress before encoding, never after β encoded output looks random and compresses terribly.
What's the difference between Base64 and Base64URL?
Base64URL swaps + for - and / for _, and usually drops the = padding, so the output survives URLs, filenames, and headers without escaping. Same algorithm otherwise, defined in RFC 4648 section 5. JWTs use Base64URL exclusively β which is why strict standard-Base64 decoders sometimes choke on them.
Why does btoa() throw InvalidCharacterError?
Your string contains characters outside Latin-1 β an emoji, an accented character, any non-Western script. btoa is a 1990s API that predates sensible Unicode handling. Encode to UTF-8 bytes first with TextEncoder, then Base64 the bytes; I shipped this exact bug in a production tool, so no judgment.
How can I tell if a string is Base64?
Valid Base64 uses only AβZ, aβz, 0β9, +, / (or -, _ for URL-safe), optional trailing =, with a length that's a multiple of 4 when padded. But plenty of ordinary words match that pattern too β cafe is valid Base64 that decodes to garbage bytes. The real test is decoding it and checking whether the output is meaningful.
Why does my Base64 string have a stray newline at the end?
Because echo adds one before base64 ever sees it, so echo "hunter2" | base64 encodes eight bytes, not seven. Use printf or echo -n instead. This is the number one cause of Kubernetes Secrets that look right in the manifest and fail at runtime: the decoded password carries an invisible trailing \n. The base64 command also wraps output at 76 columns on some systems β pass -w 0 on GNU coreutils to suppress it.
How do I decode a JWT token by hand?
Split the token at its two dots, take the first segment (header) and second (payload), and run each through a Base64 decoder β they're Base64URL, so use a tool that accepts - and _. Then format the resulting JSON in a JSON formatter to read the claims. Never paste production tokens into server-side tools; client-side only.
How do I encode an image or file to Base64?
Read the file as bytes, then Base64 those bytes and prepend a data-URI header like data:image/png;base64, so a browser can render it inline. In JavaScript, FileReader.readAsDataURL() does both steps for you; on the command line, base64 logo.png prints the raw encoding. Keep it to small assets β the 33% size tax makes Base64 a poor fit for anything large, and big data URIs bloat your HTML or CSS.
How do I decode Base64 in JavaScript, Python, or the terminal?
In modern JavaScript, decode Unicode-safely with new TextDecoder().decode(Uint8Array.from(atob(str), c => c.charCodeAt(0))) rather than bare atob. In Python, base64.b64decode(str) returns bytes β call .decode('utf-8') for text. In a terminal, echo "aGk=" | base64 -d (or --decode). All three expect standard Base64, so translate -/_ back to +// first if you're handling a Base64URL string.
The Takeaway
Base64 is a 30-year-old bit-shuffling trick that quietly holds up half the modern web β auth tokens, attachments, inline assets, secrets-that-aren't-secret. Understand the 6-bit regrouping, respect the 33% tax, never mistake it for encryption, and reach for the URL-safe variant anywhere a URL is involved. That's 95% of practical Base64 mastery.
For the hands-on part, the Base64 Converter on toolz.dev does standard and URL-safe encoding entirely in your browser β built by someone who learned the Unicode lesson the hard way, so you don't have to.
More in this series: the full coding tools guide covers the rest of the daily-driver utilities, the regex builder guide tackles the other skill developers pretend to have, and since half of Base64 debugging ends in JSON, the ultimate JSON tools guide is the natural next read.
Related Articles:

