Base64 is one of those things every developer touches but few stop to understand. You see it in data URIs, JWT tokens, email attachments, and API responses. It looks like gibberish, but it's completely predictable once you know the rules.
What Base64 actually is
Base64 is an encoding scheme that represents binary data using 64 printable ASCII characters: A-Z, a-z, 0-9, plus + and /, with = used for padding. That's it. It is not encryption, compression, or hashing. Anyone can decode it in a second. Treating it as a security measure is the single most common mistake I see.
The mechanism is simple. It takes three bytes (24 bits) at a time and splits them into four 6-bit groups. Each 6-bit group maps to one character in the 64-character alphabet. Three bytes in, four characters out — which is why Base64 output is always about 33% larger than the original.
Why 6 bits?
Six bits give you 2^6 = 64 possible values, hence the name. The whole point is to stay within a safe, printable character set that won't get mangled by systems that were designed for text and might choke on raw binary.
When you'd actually use it
The core use case is moving binary data through a text-only channel. Some concrete examples:
- Data URIs — embedding a small image directly in HTML or CSS with
data:image/png;base64,...to save an HTTP request. - Email attachments — MIME uses Base64 because the old SMTP protocol only reliably handled 7-bit ASCII.
- JSON payloads — JSON has no native binary type, so you Base64-encode a file or a byte blob to send it as a string.
- Tokens and credentials — HTTP Basic Auth encodes
user:passas Base64, and JWTs Base64url-encode their segments. - Storing binary in text-based configs — like a certificate or key pasted into a YAML file.
The rule of thumb: if the transport or storage expects text and you have bytes, Base64 is the bridge. If both ends handle binary cleanly, skip it — you're just adding 33% overhead for nothing.
A short worked example
Let's encode the string Hi!. It's three bytes, which is convenient because it maps to exactly four Base64 characters with no padding.
Text: H i !
ASCII: 72 105 33
Binary: 01001000 01101001 00100001
Regroup into 6-bit chunks:
010010 000110 100100 100001
Decimal: 18 6 36 33
Base64: S G k h
Result: SGkhNow the padding case. Encode just Hi (two bytes). Two bytes is 16 bits, which splits into three 6-bit groups with 2 bits left over. The encoder pads the last group with zeros and appends a single = to signal one missing byte:
echo -n "Hi" | base64
# SGk=
echo -n "H" | base64
# SA==One = means the final group encoded two bytes; two = means it encoded one. That padding is how the decoder knows where the real data ends.
In JavaScript, the browser and Node give you helpers directly:
// Browser
btoa("Hi!"); // "SGkh"
atob("SGkh"); // "Hi!"
// Node.js
Buffer.from("Hi!").toString("base64"); // "SGkh"
Buffer.from("SGkh", "base64").toString("utf8"); // "Hi!"When I'm debugging a token or checking what's inside a data URI, I don't bother writing a script — I paste it into a Base64 Encoder / Decoder and read it instantly.
Common gotchas
btoa and Unicode
The browser's btoa() only handles Latin-1 characters. Feed it an emoji or a non-Latin string and it throws InvalidCharacterError. You need to encode to UTF-8 bytes first:
// This breaks:
btoa("café ☕"); // Error
// This works:
btoa(String.fromCharCode(...new TextEncoder().encode("café ☕")));Node's Buffer doesn't have this problem because it works on bytes and lets you specify the charset explicitly.
Base64 vs Base64url
Standard Base64 uses + and /, which are unsafe in URLs and filenames. The URL-safe variant swaps them for - and _ and often drops the padding. JWTs use Base64url. If you decode a JWT with a plain Base64 decoder and it fails, mismatched alphabets are usually why.
It is not security
Worth repeating because it keeps happening. Base64-encoding a password, an API key, or PII gives you exactly zero protection. It's obfuscation at best, and not even that. Encode for transport; encrypt for secrecy. Those are different jobs.
The size overhead
That ~33% growth matters at scale. Inlining a 200 KB image as a data URI turns it into roughly 267 KB of markup, and it can't be cached separately from the page. For anything but tiny assets, a normal file reference is better.
Whitespace and line breaks
Some encoders (MIME, PEM certificates) insert line breaks every 64 or 76 characters. Some decoders reject unexpected whitespace. If a decode fails on data that looks valid, strip the newlines and try again.
Quick mental model
Keep these in your head and you'll rarely get caught out:
- Base64 = binary made safe for text channels, nothing more.
- Output is always a multiple of 4 characters, sometimes ending in
=or==. - It grows your data by about a third.
- Watch the alphabet: standard vs URL-safe.
- Encode UTF-8 explicitly for non-ASCII text in the browser.
It's a humble tool, but knowing how it works turns a lot of "why is my token failing" head-scratchers into a five-second fix. Once you can eyeball a string, spot the padding, and recognize the URL-safe variant, most Base64 bugs stop being bugs at all.
Photo by Mohammad Rahmani on Unsplash