ULID vs UUID: Which Should You Use for Sortable IDs?
ULID vs UUID for primary keys — ULIDs are lexicographically sortable and monotonic, so inserts stay index-friendly. Free client-side ULID generator included.
Try it now: ULID Generator — Generate lexicographically sortable ULIDs with real monotonic ordering, read the timestamp back out of one, and convert between ULID and UUID.
ULID vs UUID: What Actually Makes Them Different
The ULID vs UUID question comes down to one structural choice. ULID stands for Universally Unique Lexicographically Sortable Identifier. Like a UUID, it's a 128-bit value meant to be generated without coordination — any machine can produce one on its own, with no round trip to a central authority and effectively no chance of a collision. The difference is entirely in how those 128 bits are structured and encoded. A ULID splits its bits into two parts: the leading 48 bits are a millisecond-precision Unix timestamp, and the remaining 80 bits are cryptographically random. That timestamp prefix is the whole point — it's what makes a ULID sort in the order it was created, which a random UUID can never do.
The text encoding is also deliberately different from a UUID's. A ULID is written as 26 characters of Crockford's Base32 — no hyphens, no braces — using an alphabet that drops the letters I, L, O, and Uspecifically because they're easy to misread or confuse with 1 and 0. That makes a ULID safe to read aloud over a support call, transcribe by hand, or paste into a form without a character getting silently swapped. Base32 also packs more information per character than hex does, which is why a ULID is 26 characters against a UUID's 32 hex digits, even though both encode the same 128 bits.
01ARZ3NDEK TSV4RRFFQ69G5FAV └───┬────┘ └───────┬───────┘ timestamp randomness 10 chars 16 chars 48 bits 80 bits (ms since (generated fresh Unix epoch) each millisecond)
Because the two halves are encoded independently — 10 Base32 characters for the timestamp, 16 for the randomness — the string itself carries the split. Read the first 10 characters back and decode them as Base32, and you get the exact millisecond the ULID was generated, with no separate lookup or database column required.
Why Ordering Matters: ULID vs. a Random UUID
A version 4 UUID is 122 bits of pure randomness (plus 6 fixed version/variant bits), and that randomness is exactly the problem when it's used as a database primary key. Insert rows with a v4 key and each new value lands at a random position in a clustered B-tree index — not appended near the last one, but scattered anywhere across the whole tree. That forces the database to touch pages it hasn't written to recently on nearly every insert, which means more cache misses, more page splits as pages that were already full have to divide to make room, and an index that fragments and bloats as the table grows. None of that is a defect in UUID v4 — it's just what pure randomness does to a data structure that benefits from locality.
A ULID sidesteps this by construction. Because its first 48 bits are a millisecond timestamp, ULIDs generated in order encode in order — byte for byte, and therefore character for character once encoded to text, since Crockford's Base32 preserves ordering the same way hex does. Insert ULID-keyed rows and each new one lands next to the most recent one, the same locality an auto-incrementing integer gives you, without needing a central counter to hand out the next value. It also means ORDER BY id already returns rows in creation order, with no separate created_atcolumn or index required just to answer "what happened first."
This is precisely the same problem UUID version 7 was designed to solve — a v7 UUID embeds the identical 48-bit millisecond timestamp idea inside the standard UUID text format. The practical choice between a ULID and a v7 UUID is mostly about ecosystem and encoding, not the underlying design; the full breakdown of UUID v4 vs. v7 — including the index-fragmentation numbers and code for generating each in Python, JavaScript, and SQL — is covered in the UUID Generator guide.
Monotonicity: The Detail Naive Implementations Get Wrong
Millisecond precision sounds fine until a system generates more than one ULID within the same millisecond — which happens constantly under any real load. When that occurs, the timestamp portion of two ULIDs is identical, so sort order between them depends entirely on the 80 random bits. A naive generator re-randomizes those 80 bits independently each time, which means two ULIDs created in the same millisecond sort in whatever order the random number generator happened to produce — effectively undefined, and not necessarily the order they were actually created in.
The fix specified by the ULID spec is monotonicity: when a new ULID is generated in the same millisecond as the previous one, the random component isn't re-randomized — it's incremented by one instead, treating the 80 bits as a single large integer. That guarantees strictly increasing order for every ULID generated within one millisecond, at the cost of a tiny, practically irrelevant reduction in the entropy of that specific batch. It's the one implementation detail worth checking before trusting any given ULID library, or any generator, to actually preserve insertion order under load rather than just under a spread-out test.
// naive: random component re-randomized each call
01ARZ3NDEK 7F3A9K2M4Q1XZC8T ← generated first
01ARZ3NDEK 2B8Y1P0R6N5WVD3H ← generated second, but sorts BEFORE the first one
// monotonic: random component incremented, not re-randomized
01ARZ3NDEK 7F3A9K2M4Q1XZC8T ← generated first
01ARZ3NDEK 7F3A9K2M4Q1XZC8V ← generated second, sorts after — order preserved
(T → V: Crockford's Base32 alphabet skips I, L, O, U)ULID to UUID: A Re-Encoding, Not a Real Conversion
A ULID and a UUID are both 128-bit values — a ULID just spends those bits differently (48 timestamp bits plus 80 random bits, versus a UUID's fixed version/variant bits plus either 122 random bits for v4 or a timestamp-plus-random split for v7) and writes the result in a different text encoding. Converting one to the other is nothing more than decoding the 128 bits from one alphabet and re-encoding the same bits in the other: Crockford Base32 to hyphenated hex, or back. No timestamp is recomputed, no randomness is reinterpreted — it's the same underlying value wearing a different string.
01ARZ3NDEKTSV4RRFFQ69G5FAV ← ulid: 26 chars, Crockford Base32
↓ decode Base32 → raw 128 bits → re-encode as hex, grouped 8-4-4-4-12
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ← uuid: 36 chars including hyphens
The bits never change value along the way — only the alphabet and the grouping do.That's useful in practice mainly at the boundary of a system that expects a canonical UUID column type or client library but where IDs are actually generated as ULIDs upstream — Postgres's native uuid column type, for instance, has no ULID equivalent, so storing a ULID there means re-encoding it to the standard hyphenated form first. A ULID decoder that also splits out the embedded timestamp is the other direction of the same operation: given a ULID, read back exactly when it was generated without touching a database at all.
GenKitLab's ULID Generator generates ULIDs with real monotonic ordering (incrementing the random component within a millisecond, exactly as described above), decodes the embedded timestamp out of any ULID you paste in, and converts between ULID and UUID text in both directions. It runs entirely client-side — nothing you generate, decode, or convert is uploaded anywhere. If the choice is still between a ULID and a UUID for a specific schema, the UUID Generator produces v4 and v7 side by side for comparison.
Frequently asked questions
›What is a ULID?
A Universally Unique Lexicographically Sortable Identifier — a 128-bit value like a UUID, but structured so its first 48 bits are a millisecond Unix timestamp and the remaining 80 bits are random. It's written as 26 characters of Crockford's Base32 rather than hyphenated hex, and ULIDs generated in order sort in order as plain text.
›How is a ULID different from a UUID?
Both are 128-bit identifiers, but a random UUID v4 has no ordering at all — it's pure randomness — while a ULID's leading 48 bits are always a timestamp, so ULIDs sort chronologically without a separate created_at column. A ULID also uses a different, shorter text encoding (26-character Base32 instead of 36-character hyphenated hex) that excludes ambiguous characters like I, L, O, and U.
›How do I convert a ULID to a UUID?
It isn't really a conversion — both are the same 128 bits, just encoded as different text. Decode the ULID's Crockford Base32 string back to raw bits, then re-encode those same bits as 32 hyphenated hex digits. No data is recomputed; the timestamp and random portions carry over unchanged.
›What does 'monotonic' mean for a ULID generator?
It means that when two ULIDs are generated within the same millisecond, the random component is incremented rather than re-randomized, guaranteeing the second one sorts after the first. A generator that re-randomizes the random bits every time produces undefined ordering between ULIDs sharing a millisecond, which defeats the whole point of a sortable ID under real load.
›Can I decode the timestamp out of a ULID?
Yes — the first 10 characters of any ULID are a Crockford Base32 encoding of the millisecond Unix timestamp it was generated at. Decode just that portion and you get the exact creation time, with no database lookup needed, which is the same information a UUID v7 embeds in its own format.
›Should I use a ULID or a UUID v7 for a new database primary key?
Either solves the same underlying problem — both keep an index's most recent inserts clustered together by leading with a millisecond timestamp. Pick UUID v7 when the database column type, ORM, or client libraries already expect a standard UUID; pick ULID when a shorter, unambiguous, copy-paste-friendly string matters more than fitting a native uuid column type.
Last updated