Base64, Base32, and Base58 all turn bytes into printable text, but they solve different problems. Two chop the bitstream; one converts the whole thing as a big number — and that single distinction decides which belongs in front of a human.
The bug report reads the same every time: "The token works in Postman but breaks when I paste it into a URL." Or: "Our TOTP library rejects the secret — it's not valid Base64." Or the classic, "Why won't this Bitcoin address Base64-decode?" All three come from the same wrong assumption — that Base64, Base32, and Base58 are interchangeable ways of doing the same thing. They are not. They solve different problems, and picking the wrong one leaks into production as broken URLs, unreadable secrets, and addresses that fail to round-trip.
The confusion is understandable. All three turn arbitrary bytes into printable ASCII, and all three have "Base" in the name. But two of them (Base64, Base32) are bit-group encodings and the third (Base58) is a radix conversion — a fundamentally different operation with different performance, different output length, and a different reason to exist. Once you see that split, the choice stops being arbitrary.
Base64 and Base32 both work by slicing the input bitstream into fixed-size chunks and mapping each chunk to one character. Base64 takes 6 bits at a time (2⁶ = 64 symbols); Base32 takes 5 bits (2⁵ = 32 symbols). Because they operate on the raw bit pattern, they're fast (linear time), streamable (you can encode a gigabyte without holding it in memory), and their output length is trivially predictable from the input length.
Base58 does something else entirely. It treats the whole byte string as one enormous integer and repeatedly divides by 58, collecting remainders — the same long-division you'd use to convert a decimal number to hexadecimal by hand, just with a 58-symbol alphabet. That means it is not streamable (you can't start emitting output until you've seen the last byte, because the least-significant digit depends on the entire number), it's roughly O(n²) for large inputs, and its output length isn't a clean multiple of the input. Base58 is deliberately not built for throughput. It's built for humans.
Base64 packs 3 bytes (24 bits) into 4 characters (4 × 6 bits). Here's "Hi" — bytes 0x48 0x69 — encoded step by step:
Bytes: 0x48 0x69
Binary: 01001000 01101001
Regroup: 010010 000110 1001(00) <- last group padded with zeros
6-bit: 18 6 36
Alphabet: S G k
Output: SGk= <- '=' pads the 4-char block
The standard alphabet (RFC 4648) is A–Z a–z 0–9 + /, with = for padding. That + and / are the footgun: + becomes a space when a URL is form-decoded, and / is a path separator. So there's a second, URL-safe alphabet that swaps them for - and _. This is why the same 32-byte value shows up as a+b/c== in one system and a-b_c in another — they're both Base64, but not the same Base64. If you're putting a token in a query string, a path segment, or a JWT, you want the URL-safe variant, and you often want padding stripped too (JWT mandates no =).
Overhead: 4 output chars per 3 input bytes ≈ 33% larger. That's the cheapest of the three, which is why Base64 dominates email attachments (MIME), data URIs, and API payloads where the bytes are never meant to be read or typed by a person.
Base32 uses A–Z 2–7 — 32 symbols, 5 bits each, 8 characters per 5 input bytes. Two design choices matter. First, it's case-insensitive: the alphabet has no lowercase letters, so a human can transcribe it in any case and a phone keypad or voice channel won't mangle it. Second, the alphabet deliberately omits 0, 1, 8, and 9 to reduce confusion with O, I/l, B, and g.
That's exactly why TOTP/2FA secret keys are Base32, not Base64 — you sometimes type them by hand off a screen or read them aloud during setup. It's also why Base32 shows up in DNS-based systems, onion addresses, and some case-insensitive filesystems. The price is size: 60% overhead (8 chars per 5 bytes), nearly double Base64's. You accept the bloat because the output has to survive contact with a human or a case-folding channel.
| Base64 | Base32 | Base58 | |
|---|---|---|---|
| Alphabet size | 64 | 32 | 58 |
| Method | 6-bit groups | 5-bit groups | big-integer radix conversion |
| Bits per char | 6 | 5 | ~5.86 (variable) |
| Size overhead | ~33% | ~60% | ~37% |
| Padding | = | = | none |
| Case-sensitive | yes | no | yes |
| Streamable | yes | yes | no (whole-input) |
| Ambiguous chars removed | no | some (0189) | yes (0OIl) |
| Typical use | payloads, JWT, data URIs | TOTP secrets, DNS, onion | crypto addresses, short IDs |
Base58 starts from Base64's alphabet and removes the four visually ambiguous characters — 0 (zero), O (capital o), I (capital i), and l (lowercase L) — plus the two non-alphanumeric symbols + and /. That leaves 58: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.
Because it's a radix conversion, encoding is long division. Take the two bytes 0x27 0x0F, which is the integer 9999:
9999 ÷ 58 = 172 remainder 23 -> 'Q'
172 ÷ 58 = 2 remainder 56 -> 'y'
2 ÷ 58 = 0 remainder 2 -> '3'
Read remainders bottom-to-top: "3yQ"
Two consequences fall out of the math. There's no padding — the output is just the digits of the number, however many that takes. And leading zero bytes can't be captured by division (a leading zero contributes nothing to the integer's value), so Base58 handles them as a special case: each leading 0x00 byte is emitted as a literal 1 (the zero-index character) before the converted digits. That's why Bitcoin addresses starting with a zero byte begin with 1.
Real-world Base58 is almost always Base58Check: append a 4-byte checksum (the first 4 bytes of a double SHA-256 of the payload) before encoding. A single mistyped character changes the decoded integer, the checksum fails, and the software rejects the address instead of silently sending funds into the void. This is the same defensive instinct behind card-number checksums — catch the typo at the boundary.
Satoshi's own source comment spells out the reasoning, and it's worth taking at face value because every point is about a human copying a string:
0/O or I/l ambiguity, so an address read off a screen or a printout can't be transcribed into a different valid-looking string.+ or /, so the address survives a URL, a form field, and a filename without escaping.+ and / are word boundaries in most text engines, so double-clicking a Base64 blob grabs only a fragment. An all-alphanumeric string selects cleanly in one click.None of these matter for a MIME attachment nobody reads. All of them matter for a string a person might paste, type, photograph as a QR code, or read over the phone while losing money if they get it wrong. That's the whole design axis: Base64 optimizes for bytes-per-character, Base58 optimizes for surviving human handling, and Base32 sits in between for case-insensitive channels.
The one mistake to stop making is treating "it's just Base-something" as a detail. The encoding you pick is a statement about who — or what — is going to read the output. If it's a machine, minimize bytes. If it's a person, minimize the ways they can get it wrong. When you need to see any of these round-trip on a real value, Utilix has standalone converters for Base64, Base32, and Base58/Base62 that show the transformation both directions.
Takeaway: Base64 and Base32 chop the bitstream into fixed groups; Base58 converts the whole thing as one big number. That single distinction explains the padding, the streamability, the size overhead, and the alphabet choices — and it tells you which one belongs in front of a human and which belongs on the wire.