UUID vs ULID: Choosing the Right Identifier for Modern Applications
Every backend starts with the same decision: what do we use as the identifier? Auto-increment integers leak row counts and break in distributed systems, so most modern architectures reach for a random unique ID. That is where the real choice begins — UUID or ULID — and the difference is more practical than academic.
The problem with auto-increment, and with random UUIDs
Sequential integers have three known weaknesses: they expose business volume (order #48372 tells competitors a lot), they collide across independent systems, and merging two databases means re-keying half of your foreign keys. Random, globally-unique identifiers solve all three — which is why UUIDs became the default.
But the standard UUID v4 introduces a problem of its own: it is completely random. In a database that stores rows in primary-key order — clustered indexes, B-trees — a fully random key lands in a random page on every insert. At any meaningful table size, that turns inserts into random writes, fragments the index, and fills your buffer pool with pages that are each touched once. The symptom shows up as throughput degradation that no amount of query tuning fixes, because the problem is the key, not the queries.
The solution: time-ordered identifiers
ULID (Universally Unique Lexicographically Sortable Identifier) keeps the properties you want — 128 bits, globally unique, no coordination needed — and adds one crucial property: the first 48 bits are a millisecond timestamp.
The consequences cascade:
- Inserts are roughly sequential. New IDs are always greater than previous ones, so B-tree inserts stay in the hot rightmost page instead of jumping around the index.
- IDs sort by creation time for free.
ORDER BY idgives you chronological ordering without a separate timestamp column (accurate to the millisecond the ID was generated). - The format is human-readable-ish. ULID uses Crockford Base32 — 26 characters, uppercase, no visually ambiguous characters like
I,L,OorU.
The trade-offs are real and worth stating plainly: ULIDs encode their creation time (a privacy consideration if IDs are exposed publicly), and ecosystem support is younger than UUID's. If you need maximum compatibility — every library, every database, every protocol understands it — UUID v4 remains the safe choice. If your tables are large and write-heavy, the index-friendly ordering of a time-ordered ID is worth the switch.
Generating both, side by side
Seeing the formats is the fastest way to internalize the difference. The UUID Generator on DigDevBox produces v4 random UUIDs (plus v1, v3, v5 namespace UUIDs and the NIL UUID) with one-click copy — handy for seeding test fixtures where every row needs a unique, unpredictable key.
The ULID Generator produces time-ordered ULIDs; generate two in a row and watch the second sort after the first purely because time has passed. That property — which no random UUID can offer — is the whole argument in one demonstration.
How each format is stored
The storage story differs in ways that matter for schema design. A UUID's canonical form is 36 characters of text (8-4-4-4-12 with hyphens); a ULID is 26 characters with no hyphens. Both are really 128 bits, so the efficient storage is binary — most databases offer a native 16-byte UUID column type, and the same trick works for ULID if your tooling accepts it.
Two practical notes. First, if you store ULIDs as text, keep the default uppercase: Crockford Base32 decoding is case-insensitive, but mixed-case values break B-tree ordering assumptions in some collations — the sortability you chose ULID for quietly disappears. Second, if you migrate an existing table from random UUIDs to time-ordered IDs, new rows will sort after old ones but the old rows keep their random order; the index benefit applies gradually as old data ages out, which is fine — just do not expect an overnight throughput change.
Beyond identifiers: opaque tokens
A third category is worth separating from both: when you need a secret — an API key, a share link slug, a session token — unpredictability is the point, and neither format's structure matters. The Token Generator produces random strings from a charset you control: letters, digits, symbols, any mix, with the length you specify.
The rule of thumb: identifiers (UUID/ULID) name things; tokens authenticate access to things. Using a ULID as an API key leaks its creation time to anyone who sees it; using a structured identifier where an opaque token is expected is a design smell.
FAQ
Can I use ULID as a database primary key? Yes — it is a 128-bit value, storable as CHAR(26), BINARY(16) or as UUID type in databases that support it. Its lexicographic sort order is exactly what makes it index-friendly.
Do UUID v4 and ULID ever collide? Both are effectively collision-free at any realistic scale. ULID combines a millisecond timestamp with 80 bits of randomness; UUID v4 has 122 random bits. Generating enough values to worry about is not a practical scenario.
Is a ULID "random enough" to use as a session token? No. Its timestamp component is predictable and its entropy is lower than a purpose-generated random string. Use opaque tokens for anything security-sensitive.
Which UUID version should I use if I stay with UUIDs? v4 for most cases. Use v3/v5 only when you need deterministic IDs derived from a namespace and a name (same input always yields the same ID).
How do I convert a timestamp out of a ULID? The first 10 characters encode the millisecond timestamp in Crockford Base32. Decoding tools and library functions in every major language handle the conversion.
More developer tools on DigDevBox
- UUID Generator — v4/v1/v3/v5 and NIL UUIDs, one-click copy
- ULID Generator — sortable, time-ordered identifiers
- Token Generator — random strings with custom charsets