Hands-on base64 vs hex encode/decode lab on 32MiB buffers with real MB/s measurements in both directions on localhost.
6 min read
APIs love base64. Debuggers love hex. Both inflate payloads and burn CPU. How fast is each direction in CPython, and what do you pay in size?
This is a hands-on lab with measured numbers:
os.urandom with base64.b64encode, binascii.b2a_base64, binascii.hexlify, bytes.hex.b64decode, unhexlify, bytes.fromhex, …).Related links:
Lab honesty (1 Oct 2026 IST): Shared Linux lab box (8 cores). Python 3.13.5. Affiliates: 0. MB/s uses raw bytes as the denominator for both encode and decode so codecs compare fairly.
Verdict up front: warmed encode is a dead heat — hexlify ~668 MB/s vs b64encode ~645 MB/s. Decode is where hex wins hard (unhexlify ~881 vs b64decode ~388 MB/s). Hex still costs 2.0× size vs base64 1.333×.
| API | Direction | Output |
|---|---|---|
base64.b64encode / b64decode | both | bytes, 6-bit alphabet |
binascii.b2a_base64 / a2b_base64 | both | same idea, newline=False |
binascii.hexlify / unhexlify | both | bytes of ASCII hex |
bytes.hex / bytes.fromhex | both | str hex ↔ bytes |
Related links:
Script: lab-evidence/40-base64-vs-hex/results/run_lab.py.
| Arm | MB/s | p50 |
|---|---|---|
base64.b64encode | 645 | 49.6 ms |
binascii.b2a_base64 | 646 | 49.6 ms |
binascii.hexlify | 668 | 47.9 ms |
bytes.hex | 668 | 47.9 ms |
base64.b64decode | 388 | 82.5 ms |
binascii.unhexlify | 881 | 36.3 ms |
bytes.fromhex | 712 | 44.9 ms |
Encoded sizes on this buffer: base64 44 739 244 bytes (1.333×); hex 67 108 864 (2.000×).
Related links:
The initial matrix saw b64encode at ~469 MB/s before later stability settled ~626–635 MB/s and rebench ~645 MB/s. Hexlify stayed ~640–670 across passes. Quote the warmed encode numbers; mention the cold first sample so nobody thinks we hid variance.
Other first-pass notes: b64encode→str via .decode("ascii") fell to ~340 MB/s; urlsafe_b64encode ~331 MB/s encode / ~247 MB/s decode on that pass — URL-safe is not free.
unhexlify won big here.bytes.hex ≈ hexlify for speed; type differs (str vs bytes).Related links:
str extras unless required.Bottom line: hex is not “slow base64” on this box — it is slightly faster to encode when warm and much faster to decode, at a 50% larger ASCII footprint than base64’s 33% overhead.
Related links:
Input: os.urandom(32 MiB). Timed with time.perf_counter. Decode arms assert equality to the original buffer. Expansion recorded from encoded lengths. Rebench used 3 warmups + 15 timed calls for the headline table.
base64 / binasciiTake 32 MiB of raw API payloads you insist on ASCII-armoring:
Encode CPU is a wash; you just shipped ~21 MiB extra with hex. On decode, hex’s ~881 MB/s vs base64’s ~388 MB/s saves wall time if decode-bound — still check whether bandwidth or storage dominates. If you gzip/zstd afterward, re-bench the pair (hex often compresses differently than base64’s restricted alphabet).
JSON APIs, data URLs, and PEM-adjacent ecosystems expect base64. Hex shines in hexdumps, digests display, and some log pipelines. Do not switch encodings for a 3 MB/s encode delta; switch for ecosystem fit and size, then confirm decode path cost.
For a fixed 32 MiB buffer, encode p50 times imply whole-buffer rates:
| Codec | p50 | buffers/s |
|---|---|---|
| b64encode | 49.6 ms | ~20.2 |
| hexlify | 47.9 ms | ~20.9 |
Per-megabyte, that is the MB/s table again — useful when capacity planning says “N bulk exports/minute.” If your objects are 2 KiB JSON blobs instead of 32 MiB, fixed overheads loom larger; microbench large buffers to stress the codec, then confirm on production sizes (same advice as the orjson lab).
Also recorded on the first pass: binascii.b2a_hex 668 MB/s (alias family of hexlify) and standard_b64encode matching b64encode. Prefer the readable API; optimize only if profiles say so.
If you stream encode in chunks, expect different absolute MB/s but similar rankings; this lab is whole-buffer for clarity.
The hash and compression labs sit next to this one in a “bytes on the CPU” cluster: hash if you need integrity, compress if you need size, base64/hex if you need ASCII safety. Measure the pipeline you ship, not each stage in isolation only.
Warmed hexlify (~668 MB/s) and b64encode (~645 MB/s) are peers on encode; unhexlify (~881 MB/s) crushes b64decode (~388 MB/s). Pay 2.0× bytes for hex vs 1.333× for base64. Pick the encoding for the bottleneck you actually have — size, decode CPU, or human readability.
Evidence path on the lab box: lab-evidence/40-base64-vs-hex/results/. Affiliates: 0.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5; 32 MiB os.urandom. Rebench n=15 encode: base64.b64encode ~645 MB/s; binascii.hexlify ~668; bytes.hex ~668; binascii.b2a_base64 ~646. Decode: b64decode ~388; unhexlify ~881; bytes.fromhex ~712. Expansion: base64 1.333x, hex 2.0x. First-pass b64 colder (~469) before warm stability ~630. Affiliates: 0. Evidence: lab-evidence/40-base64-vs-hex/.
Hands-on Python json vs orjson vs msgpack lab: encode/decode MB/s on records, nested, and unicode payloads. Real p50 numbers measured on localhost box.
Summary1 Oct 20267 min read
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read
Hands-on uuid.uuid4 vs uuid.uuid1 ID generation lab: real ops/s plus version/node checks, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20263 min read