What a ULID is made of
A ULID — Universally Unique Lexicographically Sortable Identifier — is 128 bits split into two fields and written as 26 characters. The first 48 bits are the Unix timestamp in milliseconds, big-endian. The remaining 80 bits are random. That is the whole specification, and its consequences all follow from the ordering of those two fields.
Written out, a ULID looks like 01ARZ3NDEKTSV4RRFFQ69G5FAV. The first ten characters encode
the timestamp and the last sixteen encode the randomness. Because base32 maps a fixed number of bits to
each character in the same order as the underlying bytes, comparing two ULIDs as ordinary strings gives
the same answer as comparing the 128-bit integers they represent — and since time is in the high bits,
that is the same answer as comparing creation times. Sorting a list of ULIDs alphabetically sorts them
chronologically. No parsing, no separate column, no index on created_at.
The 26-character arithmetic
Base32 carries five bits per character, so 26 characters carry 130 bits — the smallest multiple of five
that covers 128. The two spare bits sit at the very top of the first character, which is why a valid
ULID never begins with a character above 7: the encodable range stops at
7ZZZZZZZZZZZZZZZZZZZZZZZZZ. A value beginning with 8 or higher has overflowed
128 bits and is not a ULID at all, no matter how well-formed it looks. This is one of the checks the
decoder above performs.
The timestamp field is 48 bits, which counts milliseconds up to the year 10889. That is deliberately generous — the field will not run out the way a 32-bit seconds counter does in 2038 — and the ten leading characters of every ULID generated in the same era share a long common prefix, which is why a batch generated together looks so visually similar.
Crockford base32, and why four letters are missing
ULIDs use Douglas Crockford's base32 alphabet:
0123456789ABCDEFGHJKMNPQRSTVWXYZ. Four letters are absent. I,
L and O are excluded because they are visually confusable with
1 and 0 — a distinction that vanishes entirely when someone reads an
identifier over the phone, copies it off a screenshot in a support ticket, or types it from a printed
invoice. U is excluded so that a random identifier is less likely to spell something
unfortunate.
That alphabet is the practical difference between a ULID and a hex-encoded identifier of the same size. Thirty-two symbols instead of sixteen means fewer characters for the same entropy, and the exclusions mean the string survives being handled by humans. The trade-off is that ULIDs are conventionally uppercase, which some people find shoutier in a URL than the lowercase hex of a UUID. Crockford's decoding rules are case-insensitive, so a lowercase ULID decodes correctly — but your database's string comparison almost certainly is not, so normalise the case at the boundary and store exactly one form.
Monotonicity: the part most implementations get wrong
A naive ULID generator draws 80 fresh random bits every time. That is fine across milliseconds, because the timestamp prefix advances and dominates the comparison. It falls apart inside a millisecond. A modern process can create thousands of identifiers in one millisecond, and all of them share the same ten-character prefix, so their relative order is decided entirely by the random tail — which is to say, decided at random.
For most uses that does not matter. For the uses ULIDs are chosen for, it does. If you are writing an event log and reading it back in identifier order, a batch inserted in the same millisecond comes back shuffled. If you are paginating with a keyset cursor — "give me the next hundred rows after this ID" — two rows created in the same millisecond can straddle the cursor, and one of them is silently skipped or returned twice. The bug is rare, non-deterministic, and appears only under load, which makes it unusually expensive to find.
The specification's answer is a monotonic generator, and it is what the Monotonic option above implements. The generator remembers the last timestamp and the last 80-bit random value. When a new call arrives in the same millisecond, instead of drawing new randomness it increments the previous random field by one, treated as an 80-bit big-endian integer. Consecutive values in the same millisecond therefore differ by exactly one in the low bits and sort in creation order by construction. When the millisecond advances, fresh randomness is drawn.
Two details make the implementation non-trivial in JavaScript. First, 80 bits do not fit in a
Number, so the increment must be done as a carry propagation across a byte array from the
least significant byte upward — not as arithmetic on a float. Second, the increment can overflow: if all
80 bits are already ones, there is no next value in that millisecond. The generator here waits for the
clock to tick rather than emitting a value that would sort backwards. The odds of reaching that state
are negligible, but "negligible" and "handled" are different things, and an overflow that silently wraps
would produce a duplicate identifier.
A third case is a clock that moves backwards, which happens on NTP corrections, virtual machine migrations and laptop resume. If the current time is earlier than the last time seen, this generator keeps using the last timestamp and continues incrementing, so ordering is preserved through the correction. Try it: switch to Monotonic, generate 1000 values, and check that they are already sorted — then switch to Plain and generate 1000 more, and you will see the same-millisecond runs come out shuffled.
ULID vs UUID v7
These two solve the same problem the same way, and choosing between them is mostly about the ecosystem you already have rather than about the identifiers themselves. Both are 128 bits. Both put a 48-bit Unix millisecond timestamp in the most significant position. Both fill the rest with randomness and both are lexicographically sortable in their canonical text forms. For the full picture of the UUID family — v1, v4, v7, the version and variant bits, and how random keys fragment an index — see the UUID generator, which is this site's reference page for UUIDs.
| ULID | UUID v7 | |
|---|---|---|
| Total bits | 128 | 128 |
| Timestamp | 48-bit Unix ms, leading | 48-bit Unix ms, leading |
| Bits available for data | All 128 | 122 — six are spent on version and variant |
| Text encoding | Crockford base32, 26 chars, uppercase | Hex with hyphens, 36 chars, lowercase |
| Ambiguous characters | None — I, L, O, U removed | None, but hex offers no such protection by design |
| Standardised by | A community specification on GitHub | IETF RFC 9562 |
| Native database column | No — store as 16 bytes or CHAR(26) | Yes, wherever a uuid type exists |
| Monotonicity | Defined in the spec as an optional generator behaviour | Optional use of the 12-bit rand_a field as a counter |
The honest summary: if your stack already speaks UUID, use v7. Every ORM, every
validation regex, every log parser and every uuid column already handles it, and RFC 9562
settles arguments that a community specification cannot. Reach for ULID when the identifier is
going to be handled by people — printed on a receipt, quoted in a support conversation, typed
into a search box, embedded in a short URL. Twenty-six unambiguous characters beat thirty-six hex
digits and four hyphens in every one of those situations.
One conversion caveat, because it bites people. A ULID's 128 bits can be rewritten as UUID text — the
hex form option above does exactly that — but the result is not a valid UUID. UUID version and
variant live at fixed bit positions, and a ULID has real data in those positions, so a strict parser
will read the version nibble as whatever the timestamp and randomness happened to put there. Round-trip
the bytes if you need to store a ULID in a uuid column, but do not expect the text form to
pass UUID validation, and do not mix the two representations in one column.
Where the sortability pays off
The reason to want a time-ordered identifier at all is almost always the database. A B-tree index over a random key scatters every insert into a different leaf page: pages split in the middle, fill factors drop, the whole index becomes the working set, and write throughput degrades as the table grows. A time-ordered key appends at the right-hand edge instead, so one page stays hot, splits are clean and index size stays proportional to the data.
Beyond raw insert performance, the time prefix buys several things for free:
- Range scans by time without a second index. "Everything created after 09:00" becomes a comparison against a synthetic ULID built from that timestamp with a zero random field — which is what the Generate at time box above is for.
- Keyset pagination that is stable under concurrent writes. A cursor of "after this
ULID" cannot skip or repeat rows the way an
OFFSETcan, provided the generator is monotonic. - Debuggability. During an incident you can read a rough creation time straight out of an identifier in a log line, with no database access. Paste one into the decoder above.
- Deterministic fixtures. Generating at a fixed timestamp produces test data whose ordering you control, which makes assertions about sort order meaningful instead of flaky.
- Sharding by time. The prefix is a natural partition key for cold-storage rollups.
The cost is the mirror image of the benefit: a ULID publishes its creation time to anyone who holds it. For an internal primary key that is fine. For a public identifier it may not be — a signup ID that reveals the exact millisecond of registration tells a competitor your growth rate, and two ULIDs side by side tell anyone which record came first. Where that matters, keep a ULID internally and expose a random identifier such as a NanoID or a UUID v4 externally.
Practical notes for API and database work
Storage. A ULID is 16 bytes. Store it as BINARY(16) in MySQL, as
bytea or a uuid column in PostgreSQL, or as a 16-byte BLOB in
SQLite. CHAR(26) is acceptable and often chosen for readability, and it is still 30% smaller
than the 36-character UUID text form — but it is 62% larger than the binary form, and in a clustered
index such as InnoDB's that cost is paid again in every secondary index.
On the wire. Send the 26-character canonical uppercase string in JSON. Binary is a storage decision, not a wire format; a base64-encoded blob in an API response helps nobody. Use the JSON formatter to check a payload full of them, and the API tester to send one to an endpoint and watch what comes back.
Validation. A correct check is 26 characters, all from the Crockford alphabet, with a
first character of 0–7. A regex of [0-9A-Z]{26} is wrong: it
accepts I, L, O and U, which no generator produces, and it accepts values that overflow 128 bits. Reject
those at the edge and answer 400 Bad Request rather than letting a
malformed identifier reach a query.
Idempotency and correlation. A ULID works well as an Idempotency-Key or a
request correlation ID, with the bonus that the identifier itself carries the time the request started —
see the HTTP headers guide for where those headers belong. If you
need a unique token identifier for a JWT's jti claim, the
JWT generator can mint one alongside the token.
Language support. Every major ecosystem has a ULID library, and the ones worth choosing expose the monotonic factory explicitly rather than hiding it. If you are auditing a dependency, the two questions to ask are whether it uses a cryptographic random source and whether repeated calls inside one millisecond are ordered. A library that fails either is producing something that only looks like a ULID.
Frequently asked questions
What is a ULID?
A Universally Unique Lexicographically Sortable Identifier: 128 bits made of a 48-bit Unix millisecond timestamp followed by 80 random bits, written as 26 Crockford base32 characters. Because time occupies the high bits, sorting ULIDs as strings sorts them by creation time.
Why is a ULID 26 characters long?
Base32 carries 5 bits per character and 26 characters carry 130 bits — the smallest multiple of 5
covering 128. The two spare bits sit at the top of the first character, which is why a valid ULID never
starts above 7.
Which letters does Crockford base32 leave out?
I, L, O and U. The first three collide visually with 1 and 0 when a value is read aloud or typed from a screen; U is dropped to make accidental words less likely.
What does monotonic generation mean?
Inside one millisecond the timestamp does not change, so plain generation orders values randomly. A monotonic generator increments the previous 80-bit random field by one instead of redrawing it, which keeps same-millisecond values in creation order.
Is a ULID the same as a UUID v7?
Structurally very close, but not interchangeable. UUID v7 spends six bits on version and variant and is written as 36 hex characters; a ULID uses all 128 bits and is written as 26 base32 characters. See the UUID generator for the full UUID reference.
Does a ULID reveal when it was created?
Yes, to the millisecond, and no key is needed to read it. Useful for internal keys and debugging; wrong for a public identifier whose creation time is sensitive.
Can two ULIDs collide?
Only within the same millisecond, since different milliseconds have different prefixes. With 80 random bits, a fifty percent collision chance needs roughly 1.2 trillion values in that one millisecond — and monotonic generation removes the risk entirely for values from one generator.
How should I store a ULID?
As 16 raw bytes where possible — BINARY(16), bytea or a BLOB —
and as CHAR(26) otherwise. Either way the ordering keeps index inserts at the right-hand
edge of the B-tree.
Are ULIDs case sensitive?
The canonical form is uppercase and Crockford decoding is case-insensitive, but database string comparison usually is not. Normalise to uppercase at the boundary and store one form.
Related tools and reference
UUID Generator
The canonical UUID reference on this site, including the time-ordered v7 that ULID is closest to.
NanoID Generator
A shorter, unordered alternative for public URLs where creation time should stay private.
Epoch Converter
Turn the millisecond timestamp a ULID carries into a readable date and back.
JSON Formatter
Check an API payload full of identifiers before you send it.
API Tester
Send a ULID to an endpoint as a path parameter or an idempotency key.
400 Bad Request
The right answer when a malformed identifier arrives at your API.