Data

What Base64 is actually for

Base64 is neither encryption nor compression: it is the adapter that carries arbitrary bytes through a text-only channel, and it makes them larger.

The first time Base64 turns up it looks like a secret: a config value trailing off in ==, a data URI in a stylesheet, the middle segment of a token. Scrambled text, so the mind supplies a reason. Encrypted, or squeezed down. Both are wrong, and the second is backwards. Base64 makes your data larger, by a fixed and predictable amount, and that fact is the key to knowing when to use it.

What it is for is narrower than either guess. Base64 is an adapter: it lets a blob of arbitrary bytes travel through a channel that only agreed to carry text, and arrive byte for byte. That is the whole job, and once the mechanism is in your head the places it shows up stop looking arbitrary.

Not a cipher, and not a compressor

It is not encryption. There is no key, nothing is concealed, and decoding is a public table lookup that ships in the standard library of every language you are likely to be using. A string ending in == is not a locked box. It is a box with the lid taped on, and the tape is labelled in a language everybody reads.

It is not compression either. Compression finds repetition and replaces it with something shorter; Base64 finds nothing and re-spells each block of input as a longer block of output, so no input comes out smaller. A JPEG or a ZIP returns a third larger than it went in, the opposite of what the dense, unreadable result suggests to the eye.

That is why the belief that Base64 conceals something is the more dangerous half of the misunderstanding. A password or an API key written into a config file as Base64 is a plaintext secret with a longer spelling, and the first thing anyone does with an unfamiliar string is paste it into a decoder.

What it does to the bits

The mechanism is small enough to hold in your head, and every property below follows from it. The encoder reads the input 24 bits at a time and slices those 24 bits into four groups of 6. Each group has 64 possible values, so each one is written as a single character from a 64-character alphabet: the upper-case letters, the lower-case letters, the ten digits, and two more symbols, the plus sign and the slash.

Since 6 bits gives 64 values and the alphabet holds 64 entries, the mapping is one to one and covers every possibility. There is no escape character, no state carried between blocks, and no substitution round to unwind. 24 bits in, 4 characters out.

The bit split, by hand

Take a string that divides neatly, Man. The encoder regroups its bytes from 8-bit columns into 6-bit columns, and each 6-bit value indexes the alphabet:

M        a        n
01001101 01100001 01101110

010011 010110 000101 101110
T      W      F      u

So Man encodes to TWFu: four characters from 24 bits, exactly as the ratio promises. The bits in the output are the bits in the input, in the same order, re-cut at different boundaries. Nothing was lost and nothing was hidden.

Most inputs do not divide evenly, and that is what padding handles. A single leftover byte produces two characters and two padding characters, so M becomes TQ==. Two leftover bytes produce three characters and one padding character, so Ma becomes TWE=. The padding carries no data; it exists so the output length is always a multiple of four, which is how a decoder knows a string is intact before it examines a character.

24bits read at a time
4characters written
64symbols in the alphabet
33%larger than the input

A third larger, always

Four characters for every 24 bits of input means the encoded form is four-thirds of the original, so about 33 percent larger, plus whatever the encoder adds for line breaks. This is not a rounding error and it is not a curiosity that only bites on huge inputs. A 100 KB file becomes roughly 137 KB of characters, and there is nothing to win back if the payload was compressed before it reached you.

Payload Bytes in Characters out Bytes added What that means in practice
1 KB10241368344An icon inlined in a stylesheet. Acceptable.
10 KB10240136563416Still reasonable inside a JSON request body.
100 KB10240013653634136Long enough that the document carrying it pays a parse cost.
1 MB10485761398104349528Send a file and reference it by path instead.
100 KB, wrapped for email10240014012837728Line breaks alone add about 3.5 KB of nothing.

The ratio is exact, and the character counts sit slightly above the bare four-thirds because an input that is not a whole number of 24-bit blocks is padded up to the next multiple of four: 1024 bytes gives 1368 characters rather than the 1365 the raw ratio implies. The last row is the same 100 KB payload wrapped for a mail transport, where every line of 76 characters ends in a two-byte line break.

The flavours that differ

There is one algorithm and a few spellings of it, and the differences change how the string behaves in the place you are about to put it. The standard form is the one in RFC 4648, and it is what a decoder means unless it has been told otherwise.

Flavour The two extra symbols Line breaks Padding Where you meet it
Standard+ and /None=Data URIs, PEM blocks, general use.
MIME+ and /Every 76 characters=Email bodies and headers.
URL-safe- and _NoneOften omittedToken segments, query strings, filenames.

All three flavours decode the same 6-bit values and produce identical bytes. They differ only in which two characters stand in for the last two alphabet positions.

MIME is the flavour that costs extra bytes. Mail transports of that era folded or dropped long lines, so the encoding wraps at 76 characters and terminates each line with a carriage return and a line feed. A 100 KB payload picks up about 1,800 lines and roughly 3.5 KB of newlines, which takes the overhead from a third to a little under 37 percent. Those line breaks are also the first thing to go wrong when a string is copied by hand, so strip them before decoding.

The URL-safe flavour exists because the plus sign means a space in a query string and the slash is a path separator, so either one left in place would be rewritten in transit. Swapping them for the hyphen and the underscore keeps the string intact through a URL, a cookie or a filename. The characters changed; the bits did not.

The channels that mangle bytes

All of this exists because text channels have always had the habit of rewriting anything that is not text. Each one carves out a set of characters it treats as structural, a delimiter or a terminator or a tag boundary, and what it does to everything else is not guaranteed to be a no-op.

Email is the historical case. Before MIME, a mail transport could be relied on to carry ASCII and little else, and some would strip bytes above 127 without saying so. Attachments were not something anyone could build on that, so the fix defined Base64 in RFC 2045 as the way to put a binary file inside a message: re-spell it in characters no transport would touch, wrap the lines, and leave the recipient instructions for unwrapping them.

JSON is the modern case and it is subtler than people expect. A JSON string is a sequence of Unicode characters and the encoding on the wire is UTF-8, so a bare byte that is not part of a valid UTF-8 sequence has no representation at all. The character escapes do not rescue you, because they name characters rather than bytes and the two are not interchangeable. XML and HTML attributes have the same shape of problem, as does any config format that promises its values are strings.

Base64 is the adapter for all of them: it turns the payload into a string of characters that every one of those channels already agrees on, without needing to know what the bytes mean. That is why the same encoding turns up in a mail body, a certificate, a stylesheet and a token.

Two properties are the whole contract. It is reversible, so the original bytes come back exactly, with no loss and no ambiguity. And it is not secret: anyone holding the string can decode it with no key, no password and no work, which is the half of the misunderstanding that matters.

Where it earns its keep

A small asset inlined in a page. A data URI lets a document carry its own icon, and the thing you win is a request that never happens. The cost is real but bounded: the asset is a third larger and it is downloaded with the document every time, which is fine for something that will never change and expensive for anything that will. Where the asset is text to begin with, such as an SVG, a percent-encoded URI from the URL Encoder carries it just as faithfully and stays readable in the source.

A certificate or a key in a value that only takes a string. The block between the -----BEGIN CERTIFICATE----- markers in your config is a DER-encoded certificate re-spelled in Base64, for exactly the reason above: an environment variable, a CI secret and a .env file are all strings, and a string cannot hold an arbitrary byte sequence. Every tool that consumes those blocks knows how to unwrap them.

A small image inside a JSON payload. If an endpoint has to accept a thumbnail and multipart uploads are not an option for whoever is calling it, an encoded field is the pragmatic answer, and both ends of the exchange will understand it without new tooling. Keep the payload small and the arrangement stays honest.

A token's payload segment. A token is three segments separated by dots: a header, a set of claims, and a signature. The header segment eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 decodes to {"alg":"HS256","typ":"JWT"}, and the claims segment is the same trick with your user data in it. Every claim is readable by anyone holding the token, which is the design rather than a flaw. What makes the token trustworthy is the signature, computed over the encoded header and payload with a key the recipient can verify.

Where people reach for it anyway

Anything large as a data URI. Past a few kilobytes the arithmetic stops being neutral. The string is a third bigger, it cannot be cached separately from the document carrying it, and it cannot be cached at all if that document is generated per request. The browser also has to decode the whole string and hold it in memory before it can use a byte of it, and in a stylesheet that wait sits in front of every rule in the file.

Hiding a password or an API key. This one keeps shipping. A secret in Base64 in a config file, a build script or a client bundle is a plaintext secret; whoever can read the string can read the key, and the encoding has added nothing except length. If a secret has to sit somewhere a client can reach, no encoding helps, and the mistake is about where the secret lives rather than how it is spelled.

An image in a database column. The column grows by a third, every backup of it grows by a third, and you give up what a filesystem does for free: range requests, a cache header, a content delivery network, and the ability to look at the thing without a query. There are narrow cases where the bytes belong in the row, such as a thumbnail that has to be committed with the record it belongs to, but they are rarer than the number of schemas that do it.

A file sent through a channel that has a binary path. An HTTP upload, a mail attachment, a copy over a socket: each takes bytes directly. Encoding a file to push it through one of them is a habit from the years when the channel really was text-only, and it costs a third of the transfer every time.

Decoding is total

Base64 holds no opinion about what it carried. Any well-formed string decodes to something; there is no checksum, no magic number and no length field that means anything beyond the padding rule. Truncate a string and the decoder either fails on the padding or returns a shorter buffer than you expected.

Change one character and you get different bytes with no warning at all, because the alphabet is dense: a single wrong character alters the 6-bit group it sits in, which is a handful of bytes in the middle of the payload, at exactly the same length as before.

So the check has to come from the payload: a length prefix, a checksum, a hash, or a signature computed over the encoded text. This is why a token carries a signature, and why a certificate is trusted because a chain validates rather than because it parsed. Base64 got the bytes across the channel, and everything you actually care about was decided somewhere else.

Where the tools fit. The Base64 Encoder takes text or a file and shows the encoded string next to the byte counts, so the third that gets added is a number on the page rather than a claim in an article. Paste an encoded string back in and it decodes, which is the quickest way to see that a token's middle segment is not protecting anything.

The URL Encoder is the same idea for a different rail: percent-encoding is how a URL carries characters that would otherwise be read as structure, which is the problem the URL-safe alphabet solves for Base64. Both pages work in either direction, taking a string in and giving the encoded or decoded form back.

The rule

Ask what the channel will accept. If it takes bytes, which a file write and an HTTP body and a socket all do, then re-spelling them as text is a step backwards and a third of the payload is the price of a constraint that no longer exists.

If the channel only takes text, and the payload is small or genuinely has to sit inside the document, Base64 is the right adapter, and it is worth saying out loud that the result is not secret. If the payload is large, or the string is meant to protect something, the answer is a different mechanism: a file and a path, or a signature. The encoding does one thing well, carrying bytes across a rail built for words.