Base64 is one of those things every developer bumps into but few stop to actually understand. You see it in email attachments, data URIs, JWTs, API tokens, and the occasional config file where someone decided plain text was too readable. It's not encryption, it's not compression, and it's not magic. It's a way of representing binary data using only printable ASCII characters.
What Base64 actually is
The idea is simple. Binary data is made of bytes, and each byte can be any value from 0 to 255. A lot of systems — email being the classic example — were built to reliably handle only text, specifically a limited set of printable characters. Send raw bytes through them and things get mangled.
Base64 solves this by taking three bytes (24 bits) at a time and splitting them into four 6-bit chunks. Each 6-bit chunk maps to one character in a 64-character alphabet: A-Z, a-z, 0-9, plus + and /. That's 64 symbols, hence the name.
Because you're turning 3 bytes into 4 characters, the output is always about 33% larger than the input. That size overhead is the price you pay for text-safe transport.
The padding character
When your input isn't a neat multiple of three bytes, Base64 pads the output with = characters so the length is always a multiple of four. One or two = signs at the end just mean the original data didn't divide evenly. They carry no data themselves.
When you'd actually use it
Base64 earns its keep whenever binary data needs to travel through a text-only channel or sit inside a text format. Common cases:
- Data URIs — embedding a small image or font directly in HTML or CSS with
data:image/png;base64,...to avoid an extra HTTP request. - JWTs — the header and payload of a JSON Web Token are Base64URL-encoded JSON. Anyone can decode them, which surprises people.
- Email attachments — MIME uses Base64 to shove binary files into a text-based protocol.
- Storing binary in JSON or XML — since these formats are text, a raw byte blob has to be encoded first.
- Basic auth headers —
Authorization: Basicis just Base64 ofusername:password.
The gotcha that trips everyone up
Base64 is not a security measure. That last example — HTTP Basic auth — makes the point. The credentials are Base64-encoded, not encrypted. Anyone who intercepts the header can decode it instantly. If you're leaning on Base64 to "hide" a value, stop. Use it over TLS, and use real encryption or hashing when you actually need secrecy.
A few other things that bite people:
- Base64 vs Base64URL. Standard Base64 uses
+and/, which have special meaning in URLs. The URL-safe variant swaps them for-and_and often drops padding. If your token round-trips fine on the server but breaks in a query string, this is usually why. - Character encoding. Base64 encodes bytes, not text. If you Base64-encode a string with non-ASCII characters, you need to decide on the text encoding (almost always UTF-8) first. Mismatched encodings produce garbage on decode.
- Line breaks. Some implementations (MIME, OpenSSL) insert newlines every 64 or 76 characters. Others produce one long string. A decoder that chokes on whitespace will fail on the wrong flavour.
- Whitespace and stripping. Copy-pasting encoded blobs can introduce stray spaces or trailing newlines that break strict decoders.
A worked example
Let's encode the three-character string Man, the canonical Base64 example. In ASCII that's the bytes 77, 97, 110, or in binary:
M a n
01001101 01100001 01101110Concatenate those 24 bits and re-split into four 6-bit groups:
010011 010110 000101 101110
19 22 5 46Map each value to the Base64 alphabet (index 0 is A, 25 is Z, 26 is a, and so on):
19 -> T 22 -> W 5 -> F 46 -> u
Result: TWFuSo Man becomes TWFu. Now try a two-byte input like Ma and you'll get TWE= — one padding character because the final group was short.
In code, most languages give you this for free:
// JavaScript (Node.js)
const encoded = Buffer.from('Man').toString('base64'); // 'TWFu'
const decoded = Buffer.from(encoded, 'base64').toString('utf8'); // 'Man'
// Browser
const enc = btoa('Man'); // 'TWFu'
const dec = atob('TWFu'); // 'Man'A quick warning on btoa/atob in the browser: they operate on binary strings and will throw on characters outside the Latin-1 range. To encode arbitrary Unicode you need to convert to UTF-8 bytes first:
const encodeUtf8 = (str) =>
btoa(String.fromCharCode(...new TextEncoder().encode(str)));Python is more forgiving because it forces you to be explicit about bytes:
import base64
base64.b64encode('Man'.encode('utf-8')) # b'TWFu'
base64.urlsafe_b64encode(b'\xff\xfe') # URL-safe variantJust decode it quickly
Most of the time you don't need to write any of this. When you're debugging a JWT payload, inspecting a data URI, or checking what's inside a Basic auth header, you just want to paste the string and see what comes out. Our Base64 Encoder / Decoder does exactly that in the browser, so nothing you paste leaves your machine — handy when the string might contain a token you'd rather not send to a random server.
The mental model to keep
Think of Base64 as a transport format, not a transformation with any inherent value. It makes binary safe for text channels, at the cost of a third more size. It's fully reversible by anyone, so never confuse it with security. And whenever something "works locally but breaks in a URL," check whether you should be using the URL-safe variant.
Get those three ideas straight and Base64 stops being a source of mysterious bugs and becomes just another reliable tool in the box.
Photo by Jake Walker on Unsplash