ULID Validation: Lowercase Input and the 128-Bit Limit
Published
Checking that a ULID has 26 Base32 characters is not enough. A validator must also enforce the 128-bit numeric limit and accept the lowercase forms allowed by the specification.
Two parsing fixes in Coola
Coola PR #2120, merged on October 23, 2026, corrects both behaviors. Previously, extract_ulid_timestamp_ms accepted 26 Z characters and returned a timestamp requiring 50 bits. It also rejected lowercase ULIDs. The patch rejects decoded values above 128 bits and makes Crockford decoding insensitive to ASCII letter case.
Why 26 characters can be too large
Each Base32 character represents five bits, so 26 characters can encode 130 bits. A ULID contains only 128: a 48-bit millisecond timestamp followed by 80 bits of randomness. The two spare bits in the text representation must be zero.
The ULID specification therefore sets the maximum valid text value to 7ZZZZZZZZZZZZZZZZZZZZZZZZZ. Once length and alphabet have been checked, requiring the first character to be a digit from 0 through 7 enforces that range. A leading 8 already exceeds it, even if every remaining character is zero.
Accept case variants, then normalize consistently
ULID decoding is case-insensitive. A lowercase or mixed-case spelling represents the same value as its uppercase equivalent. A practical validator can accept these forms and return uppercase text for consistent display, storage, and textual comparisons.
The specified alphabet excludes I, L, O, and U. Case handling does not make those letters valid. Coola also limits case conversion to ASCII, avoiding Unicode characters that could become ASCII letters during broader uppercasing. Validate the allowed input characters before using normalization to produce the canonical output.
Boundary cases for a validator
- Accept
00000000000000000000000000, the zero value, and7ZZZZZZZZZZZZZZZZZZZZZZZZZ, the maximum value. - Accept lowercase and mixed-case versions of a valid ULID and normalize them to the same output.
- Reject
80000000000000000000000000and 26 Z characters: both fit the text length but exceed the numeric range. - Reject incorrect lengths, excluded letters, punctuation, and non-ASCII lookalikes.
Decode the timestamp only after validation succeeds. A timestamp extractor is still a parser and needs the same range checks as a dedicated validation endpoint. Format validity also says nothing about whether an ID was actually generated at its encoded time or is unique in your data.
Use the ULID generator to explore the format, and read why monotonic generation needs concurrency protection when callers share state.
Browse all UUID articles →