← All Tools

Why ULID's Alphabet Skips I, L, O, U

Guide · Last verified Aug 28, 2026

Generate a few ULIDs (Universally Unique Lexicographically Sortable Identifiers) and you'll notice the letters I, L, O, and U never appear. That's not chance — it's because the encoding ULID uses, Crockford's Base32, was designed from the start to exclude those four letters from its alphabet. Let's walk through why exactly these four were dropped, and how that design choice connects to the claim that "the timestamp can represent dates up to the year 10889."

1. What Base32 is: 5 bits per character

Base32 is one of several encodings that turn binary data into a string that's easier for humans to handle. Since 32 is 2 to the 5th power, the bit stream is chopped into 5-bit chunks, and each chunk maps to one of 32 characters. That means you need 32 distinct characters, typically filled with the 10 digits (0–9) plus 22 of the 26 uppercase letters. Which 22 letters get picked is exactly where encodings diverge. Standard Base32 (RFC 4648) uses all 26 letters A–Z and reserves digits mostly for padding, while the Crockford's Base32 that ULID adopted redesigned the alphabet from scratch with a specific goal: "a human should be able to transcribe this by hand without confusion."

2. Confirmed in the actual tool code: the standard Crockford alphabet

Open the encoding table in the ULID Generator and you'll find it defined exactly like this.

ENCODING = 0123456789ABCDEFGHJKMNPQRSTVWXYZ

That's 32 characters total. Of the 26 letters A through Z, four — I, L, O, and U — are missing, and their slots are filled by the digits 0–9 to bring the count back to exactly 32. This reflects the same kind of pragmatism Douglas Crockford applied when he defined JSON in the early 2000s: a convention repeatedly validated across various systems since before 1988 (phone codes, postal codes, and the like) for "codes people actually have to transcribe by hand or read off a screen," codified here into an encoding standard.

3. Why I, L, O were dropped: the hand-transcription trap

Three of the four letters were dropped because each is easily confused with a distinct digit. Uppercase I and lowercase l are nearly indistinguishable from the digit 1 in many fonts, and uppercase O looks identical to the digit 0 in plenty of fonts too. When someone manually copies an ID from a log, reads it aloud over the phone, or retypes it from a screenshot, these three letters are the usual culprits for mistakes. Crockford Base32 removes them from the alphabet entirely, which eliminates the underlying confusion of "there are multiple characters that look like 1" at the source. Decoding, however, is typically lenient: the ULID spec commonly treats I and L as 1, and O as 0, when reading input (note that this site's decoder only accepts the exact standard alphabet as valid).

4. Why U was also dropped: not legibility, but semantics

Unlike the first three, U isn't visually confused with any other character. It was dropped for a purely different reason: to prevent accidentally generating strings that spell out profanity or offensive words. When large volumes of random character combinations are generated, as in Base32, including U raises the odds that an unpleasant word pattern shows up in English. Removing U from the alphabet altogether makes such combinations impossible from the outset. If I, L, and O were dropped for "legibility," U is the one letter dropped for a purely social reason — preventing accidental bad meaning.

5. The timestamp portion: why up to the year 10889

A ULID's 26 characters split into the first 10 (timestamp) and last 16 (random). Looking at the actual generation code, encodeBase32(ts, 10) encodes the millisecond-precision Unix timestamp into 10 characters. 10 characters × 5 bits = 50 bits of space, but the ULID spec only actually uses 48 of those bits, leaving the top 2 bits permanently zero as headroom. The maximum millisecond value representable in 48 bits is 2⁴⁸−1, which converts to a year as follows.

ItemValue
Timestamp bit width48 bits
Maximum representable milliseconds2⁴⁸ − 1 = 281,474,976,710,655 ms
Converted to years (÷1000÷60÷60÷24÷365.25)≈ 8,919 years
Maximum representable year from the Unix epoch (1970)≈ year 10889 CE
Random-part bits / characters80 bits (10 bytes) / 16 chars (two 40-bit groups → 8 chars each)

In other words, thanks to encoding 48 bits into 10 characters (50 bits of space) and leaving 2 bits of headroom, ULIDs remain millisecond-precision sortable practically forever, for all realistic purposes. For reference, the random part reflects 80 bits of genuine randomness straight from crypto.getRandomValues(), so the chance of a collision within the same millisecond is on the order of 1 in 2⁸⁰ — negligible.

6. So when do you use ULID, and when UUID

If you don't need sortability and just need global uniqueness, the familiar UUID Generator is enough. But when insertion order needs to match string sort order — as with a database primary key, where that reduces index fragmentation — ULID has the edge. If you need to generate a batch of random string IDs with a custom alphabet or length, a tool like the NanoID Generator is also worth considering.

Frequently Asked Questions

Q. Can I mix standard Base32 (RFC 4648) and Crockford Base32?

No. The alphabet mappings are different, so decoders aren't interoperable. Feeding an RFC 4648 Base32 string into a Crockford decoder produces a different value or an error. Always decode with the same scheme that was used to encode.

Q. Is a lowercase ULID still valid?

Crockford Base32 is designed to decode case-insensitively, so implementations that normalize lowercase input to uppercase before interpreting it are common. That said, uppercase output is the standard convention when generating.

Q. Can I recover the generation time from just the timestamp portion of a ULID?

Yes. Base32-decoding the first 10 characters recovers the exact millisecond-precision Unix timestamp. This is why, unlike UUID, ULID lets you reverse-engineer the approximate creation time just from the string — worth keeping in mind from a security standpoint.

Q. Doesn't dropping I, L, O, U reduce the available character space?

Base-32 itself only needs exactly 32 distinct symbols, not specifically 26 letters, so combining 10 digits with 22 letters still fills all 32 slots. If anything, the core of this design is that dropping confusable characters actually increases real-world reliability.