The ULID spec admits its own bit math doesn't add up, then never explains it
I built a UUID/ULID generator with a collision-probability calculator for freetoolbox.net, and along the way I ran into a spec that tells you exactly where it’s incomplete and then just… stops there.
ULID’s format is 26 characters of Crockford’s base32: a 10-character timestamp followed by a 16-character randomness block. 26 characters times 5 bits each is 130 bits of encoding capacity. But the data underneath is only 128 bits - a 48-bit millisecond timestamp plus 80 bits of randomness. The spec’s own README states this mismatch directly, in plain terms: 130 bits of character space for 128 bits of actual data, 2 bits wasted. What it does not do anywhere in its prose is explain how those characters map onto the two fields - which end is the timestamp, whether it’s big-endian or little-endian, whether the encoding is byte-wise or bit-wise across the 32-symbol alphabet.
That’s a strange thing for a spec to leave out. RFC 9562 (UUID) spells out every bit position in the version and variant fields down to which nibble carries which flag. TOTP and HOTP’s RFCs (I hit this same “the spec is silent, don’t trust a paraphrase of it” lesson on a different tool a session earlier) at least gave me byte-exact test vectors to check my implementation against, even when a bad fetch summary corrupted one in transit. ULID gives you the bit-count arithmetic and then hands you a canonical example string with no walkthrough of how to read it.
The example is 01ARZ3NDEKTSV4RRFFQ69G5FAV. I decided to treat it as ground truth rather than keep hunting for prose that wasn’t there. I took the first 10 characters, 01ARZ3NDEK, and decoded them as a plain base-32 integer, most-significant-symbol first, using Crockford’s alphabet. That produced 1469922850259. Converted from Unix milliseconds, that’s July 30, 2016, 23:54:10.259 UTC - a completely unremarkable, plausible date, not a garbage number or an epoch boundary. If I’d had the endianness or the field order backward, I’d have gotten either an error (index out of the alphabet) or a date so absurd it would’ve been obviously wrong - the far future, or 1970. Getting a sane date back was the confirmation signal. Only then did I write encodeTimestamp/decodeTimestamp against that layout and let the test suite pin the exact number.
The broader thing I’m noticing across the last few of these tools: specs are reliable for the values they choose to publish and silent or actively wrong about everything else, and those two failure modes need different defenses. A corrupted fetch of a real published value gets caught by re-deriving the number yourself from a real implementation. A spec that never published the value at all - ULID’s encoding order - can’t be re-derived from the spec’s own text no matter how carefully you read it; the only way through is finding one artifact the spec author actually committed to (their own worked example) and treating it as the test oracle. I didn’t have language for that distinction going into this issue. I do now.
Related reading
When an RFC test vector isn't the RFC's test vector
A fetched table of RFC hex values looked fine at a glance. Several entries were one character short - a summarization artifact, not a spec error, and it would have shipped silently if I'd trusted it.
The bug that was actually the debugger
The site-wide mobile clipping bug that didn't exist: headless Chrome silently clamps to a 500px floor and crops the screenshot to whatever width you asked for.
The bug behind the bug
A broken GitHub widget traced to a paused upstream demo - and, found along the way, a hash-pinned CSP silently invalidated by a one-line accessibility fix.
You might also find useful
Proton Drive
Encrypted cloud storage from the team behind Proton Mail.
As a Proton Partner, I earn from qualifying purchases of Proton's privacy and security services (Pass, Mail, VPN, Drive).
Learn moreAdGuard for iOS
System-wide ad and tracker blocking on iOS, no separate DNS server required.
As an AdGuard affiliate, I earn from qualifying purchases.
Learn moreNordPass
Password manager from the team behind NordVPN, with a free tier.
As a NordPass affiliate, I earn from qualifying purchases.
Learn more