HEX vs Base64 vs Base58 — picking a binary-to-text encoding
Compare hex, Base64, and Base58 for hashes, tokens, IDs, and wallet addresses by size efficiency, URL safety, readability, and use case. Why Base58 became the standard for cryptocurrency addresses.
Four axes for picking a binary-to-text encoding
Whenever you carry binary data — a hash, a key, an image, a token — through a text channel, you reach for a binary-to-text encoding. The three you meet most often are HEX (Base16), Base64, and Base58. The choice between them comes down to four axes: size efficiency (how much does the encoded form bloat?), character set (which characters appear in the output?), URL safety (can you drop the string into a query string or JSON without escaping?), and human readability (can someone read it aloud or copy it on paper without mistakes?).
There is no global ranking — “Base64 is best” or “everyone is moving to Base58” are both wrong. A SHA-256 hash you want to grep in logs is happiest as HEX. An image embedded in an email is happiest as Base64. A Bitcoin address printed on a paper wallet is happiest as Base58. Size alone would favour Base64 every time, but real-world friction — + breaking inside URLs, 0 versus O confusion when read aloud — pushes the answer around.
Side-by-side comparison
| Property | HEX (Base16) | Base64 | Base58 |
|---|---|---|---|
| Alphabet size | 16 | 64 | 58 |
| Characters | 0-9, a-f | A-Z, a-z, 0-9, +, / | A-Z, a-z, 1-9 (excludes 0, O, I, l) |
| Padding | None | = to 4-char boundary | None |
| Bits per char | 4 | 6 | ~5.86 |
| Size overhead | 200% (2x) | ~133% (4/3x) | ~137% (1.37x) |
| URL safe | Yes | No (+, /, =) | Yes |
| Ambiguous chars | None | Symbols +, / | Removed by design |
| Case sensitivity | Effectively none | Yes | Yes |
| Typical uses | Hashes, MAC addresses, hex dumps | Email (RFC 2045), data URIs, JWT | Bitcoin addresses, IPFS CID v0, Solana |
| Standard | RFC 4648 | RFC 4648 | de facto (from Bitcoin) |
Size overhead is the ratio of encoded length to raw byte length. A 32-byte SHA-256 digest takes 64 characters in HEX, 44 characters in Base64 (with padding), and roughly 44 characters in Base58. Base64’s = padding rounds the input to a multiple of three bytes; watch out for the difference between MIME (76-char line wrap) and PEM (64-char line wrap) flavours when parsing. Base58 deliberately drops 0 (zero), uppercase O, uppercase I, lowercase l, and the symbols + and /. That choice trades a tiny amount of efficiency for far better behaviour when humans copy the string on paper, dictate it on the phone, or read it from a QR-code recovery card.
Use case → recommended encoding
File hashes, MAC addresses, raw byte dumps: HEX. The 2x size hit is the price, but no other encoding lets you eyeball “a3 b5 7f…” groups the way HEX does, and every standard tool (md5sum, sha256sum, xxd, hexdump) already speaks it.
API tokens, email attachments, data URIs: Base64. Best size efficiency, fits cleanly in SMTP, HTTP headers, and JSON fields. JWT is just three Base64URL-encoded segments separated by dots (header.payload.signature). The big caveat — Base64 and Base64URL are not the same encoding. Standard Base64’s + / / / = collide with URL syntax, so anything that travels through a URL must use Base64URL (- / _, padding optional).
Short shareable identifiers / strings users will double-click to copy: Base64URL or Base58. Base58 stops at [1-9A-HJ-NP-Za-km-z], with no hyphen or underscore — that means a double-click in your editor or browser address bar selects the entire token, while a Base64URL string containing - gets chopped at the dash.
Cryptocurrency addresses, content identifiers: Base58. Bitcoin addresses and IPFS CID v0 (the Qm... form) use Base58Check, a variant that appends the first four bytes of SHA-256(SHA-256(payload)) as a checksum. Mistype a single character and the wallet refuses the address. Solana account addresses are Base58 too, though without the Base58Check checksum.
Browser-only conversion and the usual traps
Real day-to-day work means converting between these formats constantly — “this Base64 string in the server log should be HEX in the iv: field”, “decode the JWT payload from Base64URL”. For straight Base64 encode and decode, base64 is the shortest path. For switching between Base16 / Base32 / Base58 / Base64 (and recoding from one to another), use base-codec. Both run entirely in the browser, so private keys and API tokens you paste in never leave the page. The implementations live on GitHub, and the DevTools Network tab confirms the absence of outbound requests.
Three traps worth memorising. First, standard Base64 cannot live inside a URL unmodified: + decodes to space, / is a path separator, = gets clipped as a form value. Either emit Base64URL from the start or pipe through encodeURIComponent. Second, never compare hex hashes as strings without case-folding. Most APIs treat 0xDEADBEEF and 0xdeadbeef as the same value, but a string-level === between two SHA-256 digests will report a false mismatch the moment one source uses uppercase and another lowercase. Decode to bytes for comparison, or normalise with toLowerCase() first. Third, not all Base58 alphabets are the Bitcoin alphabet: Ripple and Flickr each defined their own permutation. A “Base58” decoder using the wrong alphabet still produces output, just silently wrong output. Always confirm the alphabet against the receiver’s specification before sending anything across.