uuid
UUID vs ULID: v4, v7, and Which ID You Should Actually Use
A practical comparison of UUID and ULID for generating unique IDs — how UUIDv4, UUIDv7, and ULID differ, why random IDs wreck database index performance, what "sortable" really buys you, and a clear decision guide for picking the right identifier in 2026.
You need a unique ID for a new row. You reach for uuid() out of habit, and it works — until your table hits a few million rows and inserts start getting slower for no obvious reason. The ID format you picked is part of the story. Here’s what actually differs between UUIDv4, the newer UUIDv7, and ULID, and how to choose.
What they all have in common
Every one of these is a 128-bit identifier you can generate client-side, with no coordination — no central server handing out numbers, no risk of two machines colliding in practice. That’s the whole appeal over an auto-increment integer: you can mint an ID before the row ever reaches the database.
Where they differ is how the 128 bits are arranged — and that arrangement decides whether your IDs are random noise or roughly time-ordered.
UUIDv4: 122 bits of randomness
UUIDv4 is the one most people mean when they say “UUID.” It’s almost entirely random: 122 random bits, with 6 bits reserved to mark the version and variant.
f47ac10b-58cc-4372-a567-0e02b2c3d479
^ ^
version 4 variant bits
Collision odds are famously negligible — you’d need to generate billions of UUIDs before a collision became likely. For correctness, v4 is great.
The problem is ordering. Two UUIDv4s generated a millisecond apart are completely unrelated values. When you use one as a primary key, every insert lands at a random position in the table’s B-tree index. That causes:
- Index fragmentation — pages split all over the tree instead of appending at the end.
- Poor cache locality — recently inserted rows aren’t near each other on disk, so range scans and “most recent” queries touch many more pages.
- Larger indexes — random keys compress poorly.
On a small table you’ll never notice. On a hot, high-insert table it’s a real, measurable tax.
UUIDv7: random, but time-sortable
UUIDv7 (standardized in 2024’s RFC 9562) fixes the ordering problem while staying a valid UUID. It puts a 48-bit Unix millisecond timestamp in the high bits, then fills the rest with randomness.
018f9c8e-7b2a-7c3d-9e4f-1a2b3c4d5e6f
└──────timestamp──────┘└──random──┘
Because the timestamp leads, lexicographic order matches creation order. New IDs sort after old ones, so inserts mostly append to the end of the index — the same access pattern that makes auto-increment integers fast, without giving up client-side generation.
UUIDv7 is a drop-in replacement anywhere you already store a uuid type. If your database and driver support it, it’s the modern default for new primary keys.
ULID: the same idea, a friendlier string
ULID predates UUIDv7 and solves the exact same problem: a 48-bit timestamp followed by 80 random bits. The difference is encoding.
| UUID | ULID | |
|---|---|---|
| Bits | 128 | 128 |
| Canonical text | 36 chars, hex + dashes | 26 chars, Crockford Base32 |
| Example | 018f9c8e-7b2a-7c3d-... | 01HZ9C8E7BQ8... |
| Case sensitive | yes (hex) | no (Base32) |
| URL-safe | needs care with dashes | yes, no special chars |
| Sortable | only v7 | always |
ULIDs are shorter, case-insensitive, and copy-paste cleanly into URLs. The trade-off is that they’re not a valid UUID — if your database has a native 16-byte uuid column type and tooling built around it, a UUIDv7 slots in more naturally than a ULID, which you’d typically store as text or raw bytes.
You can mint either format instantly with the UUID generator and the ULID generator to see the shapes side by side.
A subtle gotcha: timestamps leak
Both UUIDv7 and ULID embed the creation time in the ID. Anyone holding the ID can extract roughly when the record was created. For most internal records that’s fine — even useful. But for IDs you expose publicly (password-reset tokens, invite codes, anything security-sensitive), a time-ordered ID also makes the next value somewhat guessable in range. For those, prefer fully random v4, or a dedicated API key generator with proper entropy, not a sortable ID.
The decision guide
- Primary keys on a high-insert table? UUIDv7 (or ULID if you want the shorter string and don’t need native
uuidtype compatibility). The time-ordering keeps inserts cheap. - Public, unguessable tokens? UUIDv4 — you want the lack of order and patterns. Better yet, use a purpose-built secret/token generator.
- You just need a unique value and don’t care about index behavior? Any of them. v4 is everywhere and zero-config.
- Distributed systems that need lexical sort without a coordinator? UUIDv7 or ULID — that’s the exact use case they were designed for.
The short version: if it’s a database key, reach for UUIDv7. If it’s a secret, reach for v4. The old reflex of “v4 for everything” is the one worth retiring.
If you’re choosing IDs as part of a broader API design, it pairs naturally with thinking about how JWTs work for the tokens that carry them.