6 min read

Base64, Hex, and Percent-Encoding Compared

Base64, hexadecimal, and percent-encoding all turn awkward bytes into characters that survive a text channel. They are routinely confused, and choosing the wrong one produces corrupted values, broken links, and a false sense of security.

They solve the same problem at different sizes

All three encodings exist because some channel cannot carry arbitrary bytes. Base64 packs three bytes into four printable characters, a 33% expansion, which makes it the practical choice for embedding binary data in JSON, email, or a data URI. Hexadecimal spends two characters on every byte, doubling the size, but each byte is readable on its own, which is why digests, protocol dumps, and debugging output use it.

Percent-encoding is different in kind. It leaves safe characters untouched and escapes only the ones that would otherwise be read as URL syntax, so its size depends entirely on the content. That makes it the right tool inside a URL and the wrong tool for binary data, where nearly every byte would need escaping.

Recognising which one you are looking at

A string of only 0-9 and a-f with an even length is hexadecimal; if it is exactly 32 or 64 characters it is very likely an MD5 or SHA-256 digest rather than encoded text. A string built from mixed-case letters, digits, plus and slash, possibly ending in one or two equals signs, is Base64. A string peppered with percent signs followed by two hex digits is percent-encoded.

Two special cases are worth memorising. A value starting with eyJ is Base64 of JSON, because {" encodes to that prefix, and three such segments separated by dots is a JSON Web Token. A value containing %25 has been percent-encoded twice, because %25 is itself an encoded percent sign.

None of them is encryption

Every encoding on this list is public and reversible without a key. Encoding a password, an API key, or a customer record changes how it looks and not who can read it. Treat an encoded secret exactly as you would treat the plain value: over TLS in transit, in a secret manager at rest, and out of logs entirely.

The related mistake is assuming an encoded value has been validated. Decoding produces bytes, nothing more. Those bytes still need parsing, schema checking, and, for anything security relevant, a signature check performed by the server.

  1. Identify the encoding from its alphabet before you try to decode it.
  2. Decode once, then check whether the result still looks encoded.
  3. Parse the decoded value with a real parser rather than reading it by eye.
  4. Confirm that nothing in the chain is treating the encoding as a security control.