DevKitLab Logo DevKitLab
Base64 / Encoding / Debugging

What Is Base64, and Why Won't Mine Decode?

You paste a Base64 string into a decoder and get 'Invalid character' — or worse, it succeeds and hands you a screen of mojibake. Usually nothing is corrupted. You've made a category error about what Base64 is: not encryption, not compression, just a reversible way to carry bytes through text-only channels — and once you see that, every decode failure becomes one of a short, nameable list.

You paste a Base64 string into a decoder and it spits back Invalid character. Or worse — it succeeds, and hands you a screenful of mojibake. So you try a different tool and it works; you try the browser’s atob() and it throws; you decode on the command line and get something subtly different again. By now you suspect the string is corrupted and you go hunting for where it got truncated. Almost always, nothing is corrupted. You’ve just made a category error about what Base64 is.

Here’s the reframing: Base64 is not encryption, not compression, and not a checksum. It’s a reversible way to repackage arbitrary bytes into 64 characters that survive text-only channels. It answers exactly one question — “how do I carry raw bytes through something that only accepts text?” — and it makes no promise about secrecy (anyone can decode it) or size (it grows the data by about a third). Once you hold that, “why won’t it decode?” stops being a mystery and splits into a short list of concrete mismatches: a different alphabet, missing padding, a space that used to be a +, or bytes that decoded perfectly and only look like garbage because they were never text to begin with.

This article spends a few minutes on what Base64 actually is — three bytes in, four characters out — and then walks every common reason a decode fails or misleads, from the swapped-alphabet variant that trips up many hand-decoded tokens to the “it decoded fine, it just isn’t text” trap that isn’t really a failure at all. By the end you’ll have a checklist you can run against any string that won’t come apart.

The one idea: a byte-to-text repackaging, not a lockbox

Hold two things apart and most of the confusion clears. Base64 transforms bytes into text and back — losslessly, and with no key. It exists because so many channels are defined for text and will mangle or reject raw binary: a URL, an HTTP header, a JSON string, an email body, a data: URI. Drop a stray null byte or a high-bit character into any of those and it breaks. Base64 launders arbitrary binary into a safe 64-character subset — A–Z, a–z, 0–9, +, / — that passes through most of them intact. (One caveat: standard Base64’s own +, /, and = still carry meaning in a URL query or an HTML form, so those two cases specifically want the URL-safe variant or an extra round of URL-encoding — the wrinkle behind Failure 3 below.)

Two consequences to burn in, because they’re the source of half the misconceptions:

  • It’s public. The transform is fully reversible by anyone, with no secret involved. Base64-encoding a password or an API key does not protect it — it just makes it slightly less obvious to a human skimming. If you need secrecy, you need encryption; Base64 is a costume, not a lock.
  • It’s bigger. Every 3 bytes become 4 characters — about 33% overhead. It is the opposite of compression. People sometimes reach for it hoping to shrink data; it does the reverse.

How it actually works: 3 bytes in, 4 characters out

The mechanic is worth thirty seconds, because it explains most of the failures. Base64 chops the input into groups of 3 bytes (24 bits), then re-slices each 24 bits into four 6-bit numbers, and maps each 6-bit value (0–63) to one character of the alphabet. Three bytes always become four characters:

Man  →  01001101 01100001 01101110   (3 bytes = 24 bits)
        010011 010110 000101 101110  (four 6-bit groups)
        T      W      F      u          → "TWFu"

When the input isn’t a multiple of 3 bytes, the final group is short, and Base64 pads the output up to a multiple of 4 with =: one leftover byte becomes two characters plus ==, two leftover bytes become three characters plus =. That = is not data — it’s alignment. This is why a standard Base64 string’s length is always a multiple of four, and why the failures below cluster around exactly three things: the alphabet, the padding, and the length.

Failure 1: it’s Base64URL, not Base64

A very common decode failure. Base64URL is a variant built to survive URLs and HTTP headers, and a standard decoder chokes on it. Two differences matter:

  • + becomes -, and / becomes _.
  • The trailing = padding is usually dropped.

(Whether the text is wrapped into fixed-width lines is a separate, transport-level choice — not part of the Base64URL alphabet — though URL-safe strings are conventionally sent unwrapped.) The cruel part is that + and / are exactly the characters most likely to show up in a hash, a key, or a signature — so the corruption is both common and quiet. This is a common trap when decoding JWTs by hand: a token’s three segments are Base64URL, so feeding one to a standard decoder throws or returns garbage — a mismatch that derails many “let me just decode my token” attempts. The fix is to translate first — swap -/_ back to +// and re-pad to a multiple of four — or use a decoder with a URL-safe mode. In a Base64 tool, run the same string through URL-safe versus standard mode and watch one read cleanly while the other errors on the very same input.

Failure 2: padding

Padding is the other half of that story. A Base64 string whose length isn’t a multiple of four may just be missing its = padding — or it may be malformed or truncated — and decoders disagree about what to do. Strict ones throw Invalid length; lenient ones infer the missing padding and succeed. That disagreement is exactly why the same string “decodes” in one tool and fails in another, which sends people chasing a corruption that isn’t there. The length modulo four tells you which case you’re in: a remainder of 0 is already complete; 2 means add ==; 3 means add a single =; and a remainder of 1 is not a padding problem at all — it’s an illegal or truncated string, because a valid Base64 group never leaves exactly one extra character. (Padding only ever appears at the very end, at most two =, and never in the middle — an = mid-string is itself a sign of corruption.) And since Base64URL strips padding on purpose, this failure and Failure 1 usually travel together — a URL-safe string is both re-lettered and unpadded.

Failure 3: a space that used to be a +

The sneaky one, and it’s where this article reaches into the next. If a Base64 string rode through a URL query string or an HTML form, its + characters may have been turned into spaces — because in application/x-www-form-urlencoded, a + means a space. Now your decoder sees where a + belonged and either errors or produces the wrong bytes. Stray newlines do the same kind of damage: some MIME or PEM formats insert line breaks (commonly every 64 or 76 characters, depending on the format), and those breaks (like any copy-paste whitespace) aren’t part of the data. The fix depends on where it came from, and the order matters: first decide whether it went through form encoding — if it did, turn the spaces that were really + back into + — and separately strip the newlines and copy-paste whitespace that transport inserted. Don’t blindly convert every space to a +; only the form-encoded ones were ever pluses. This is really a URL-encoding problem wearing a Base64 mask. Base64URL sidesteps this whole class of bug by never using + at all.

Failure 4: it decoded fine — it just isn’t text

The failure that isn’t a failure. Sometimes the decode works and you still get garbage, so you conclude the string was bad. It wasn’t. Decoded bytes are not required to be readable text. Base64 carries arbitrary bytes: the original might have been a PNG, a gzip stream, a protobuf message, or an encryption blob, and rendering those raw bytes as text produces exactly the mojibake you’d expect. Even when the payload really is text, it’s text in some encoding — decode UTF-8 bytes as Latin-1 and café comes back as café. So before you rule a decode “failed,” ask what the bytes are; the first few often announce it (\x89PNG for a PNG, PK for a zip, { for JSON). “I can decode it” and “I can read it” are two separate claims — the same split you meet in a JWT, whose signature segment is Base64URL over raw bytes that aren’t text at all. Paste one into the JWT inspector and the header and payload decode to JSON while the signature stays deliberately opaque. (In the browser, note that atob() hands back a binary string — one character per byte — not decoded UTF-8 text; to turn UTF-8 bytes into readable characters you still have to run them through a TextDecoder.)

Failure 5: double-encoding and the data: prefix

Two quick ones that account for a surprising number of stuck decodes:

  • Double-encoding. A value gets Base64’d twice, so your first decode returns another Base64 string instead of the payload — decode again. The tell is a decode that yields clean, Base64-looking ASCII rather than the content you expected.
  • The data: URI prefix. data:image/png;base64,iVBORw0KGgo... is not all Base64 — only the part after the comma is. Feed the whole data:...;base64, string to a decoder and it errors on the : and ;. Strip everything up to and including the comma first.

The checklist for a Base64 that won’t decode

When a string refuses to come apart, don’t assume it’s truncated. Run these in order:

  1. Whitespace and transport. Strip the newlines and copy-paste whitespace transport inserted. Separately, if it went through form encoding, restore the + that became spaces — but don’t blanket-convert every space (Failure 3).
  2. Alphabet. See any - or _? It’s Base64URL — translate to +//, or switch the decoder to URL-safe mode (Failure 1).
  3. Padding and length. Check the length modulo four: remainder 2 → add ==, 3 → add a single =. A remainder of 1 means the string is malformed or truncated, not just unpadded — don’t blindly pad it (Failure 2).
  4. Prefix. Strip a leading data:...;base64, before decoding (Failure 5).
  5. Decode, then ask what the bytes are. If it “worked” but reads as garbage, the payload was binary or a different charset — not a decode failure at all (Failure 4). If your first decode is itself Base64, decode again.

Underneath all five is the one idea: Base64 is a reversible byte↔text repackaging, nothing more. It doesn’t hide your data, it doesn’t shrink it, and it doesn’t guarantee the result is readable — and a successful decode only means the characters mapped back to bytes, not that the content is complete or unmodified. Most “won’t decode” bugs aren’t corruption — they’re a wrong alphabet, missing padding, a + that became a space, or bytes that were never text. Name which one it is, and the string that looked broken comes apart cleanly.