Base64 vs Hex, Base32 and ASCII85: Encodings Compared

2026-09-17 · 1148 words

Base64 is the default choice for turning bytes into text, but it is not the only one and it is not always the right one. Hexadecimal is easier to read and worse at everything else. Base32 is bigger and survives environments that destroy case. ASCII85 is smaller and appears in places you would not expect. Each exists because of a specific constraint, and picking the wrong one is usually a decision nobody made on purpose.

The four, side by side

| Encoding | Alphabet | Chars per 3 bytes | Overhead | Case-sensitive | |---|---|---|---|---| | Hex | 0-9a-f | 6 | +100% | No | | Base32 | A-Z2-7 | 8 (per 5 bytes) | +60% | No (by convention uppercase) | | Base64 | A-Za-z0-9+/ | 4 | +33% | Yes | | ASCII85 | ! to u | 5 (per 4 bytes) | +25% | Yes |

The overhead column is arithmetic, not preference. Hex spends exactly two characters per byte, so it doubles the data. Base32 packs 5 bits per character, so it needs 8 characters to carry the 40 bits of 5 bytes — 60% overhead. Base64 packs 6 bits into 4 characters for 3 bytes. ASCII85 packs 4 bytes into 5 characters, the most efficient of the four; it is what PostScript and PDF use for embedded streams.

Hex: for humans and for hashes

Hex is the least efficient encoding here and the most useful one when a person is going to look at the data. Every byte maps to a fixed pair of characters, so the mapping is memorisable and offsets are trivially readable: byte 4 of a file is characters 8 and 9 in the hex dump, no arithmetic required.

const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');

That property is why hashes, checksums, MAC addresses, UUIDs and file signatures are all written in hex. When you read 89 50 4E 47 at the start of a file, you are reading PNG's signature in the form the format specification uses, and there is no translation step between the documentation and your hex dump.

The failure mode to know: hex is not Base64, and decoders rarely complain when you confuse them. A string of 0-9a-f is a subset of both alphabets, so Buffer.from(hexString, 'base64') returns bytes without error — just not the bytes you meant. It is one of the few cases where a decoder succeeds and the answer is simply wrong, which makes it worth checking before anything else when a payload "decodes but makes no sense".

Base32: when case cannot be trusted

Base32 uses 32 uppercase letters and digits, and it exists for environments where case is destroyed or confusable: DNS names, some barcode and QR workflows, serial numbers read aloud, and systems documented as case-insensitive. Its 60% overhead is the price of removing ambiguity.

import base64
print(base64.b32encode(b'hello').decode())   # NBSWY3DP

The alphabet also excludes 0, 1, 8 and 9, along with I, L, O and U in the extended variant, which removes the character pairs people misread. If a human has to transcribe a token by hand, that matters more than the extra 27 percentage points of size. If a machine is transferring it, base32 is a strictly worse choice than Base64.

Base64: the default, with two alphabets

The reason Base64 dominates is that it hits the best available compromise: 6 bits per character is the most you can carry while keeping every character printable, and 64 is the largest power of two that fits in the printable ASCII range. Its two problems — / and + colliding with URL syntax, and case sensitivity — are solved by the URL-safe variant and by knowing your transport.

The alphabet itself is the source of most Base64 pain in practice: a single copied l for I, or O for 0, produces a string that decodes without complaint into the wrong bytes. That is why implementations report the character position and code point when they reject input, and why "check the code point" is better advice than "check the character" when two symbols look alike.

ASCII85: smaller, and mostly invisible

ASCII85 encodes 4 bytes as 5 characters using a 85-symbol alphabet covering ! through u, giving 25% overhead instead of Base64's 33%. The saving is real but modest, and the cost is a much less forgiving format: no padding mechanism in the usual sense, special rules for long runs of zero bytes, and an alphabet that includes characters needing escaping in PostScript and XML.

You are most likely to meet it inside a PDF or a PostScript file rather than to choose it. If you do need to decode one, Python has it built in — base64.a85decode — and it is worth knowing that PDF also defines ASCIIHexDecode, so a PDF stream filter chart is a place where three of these four encodings appear side by side.

Choosing, in one paragraph

Use hex when a person or a specification will read the bytes, and when fixed-width readability matters more than size: hashes, signatures, binary protocol dumps, colour values. Use Base64 for everything else that moves through text — JSON, XML, email, HTTP bodies, data URIs, embedded assets — and switch to its URL-safe alphabet when the string will appear in a URL, a filename or a cookie. Reach for Base32 only when case sensitivity or human transcription is the binding constraint. Choose ASCII85 when you are implementing a format that already uses it, and not otherwise.

Converting between them

All four are lossless round trips, so conversions are safe — but each conversion is a place where a mistake hides, because every one of these encodings produces strings that look plausible in the other alphabets.

const toHex = (bytes) => Buffer.from(bytes).toString('hex');
const toB64 = (bytes) => Buffer.from(bytes).toString('base64');
const toB64Url = (bytes) => Buffer.from(bytes).toString('base64url');
const fromHex = (s) => Buffer.from(s, 'hex');
const fromB64 = (s) => Buffer.from(s, 'base64');

// a hex string decoded as base64: no error, wrong bytes
fromB64(toHex(Buffer.from('hello'))).toString('utf8')   // not 'hello'

The safest habit is to convert bytes, never strings: decode to bytes, then re-encode from those bytes. Converting a hex string directly to Base64 by string manipulation works, but it silently assumes both encodings are in their canonical form — no whitespace, no line breaks, correct padding — and the assumption fails exactly when something upstream did something unusual.

If you want to see what a given payload actually is before choosing a decoder, the answer usually comes from the first few bytes rather than the alphabet: a hex-looking string that starts with 89 50 when decoded as hex is a PNG, and image file signatures lists the rest. And if the goal is simply to see the image, Base64 to Image will try the plausible interpretations in order and tell you which one worked.

One more comparison is worth carrying in your head, because it decides how much size matters: Base64 and ASCII85 are for moving arbitrary bytes through text, hex is for reading bytes, and Base32 is for transcribing them. Only the first two are about efficiency, and between them the 8-point difference (33% versus 25%) is rarely the deciding factor — the surrounding format usually is. If you are fighting size rather than encoding, the more useful question is whether the data should be text at all; why Base64 makes files 33% bigger covers where inlining stops paying for itself.

Related reading

Try the Base64 to Image converter →