Generate two IDs a few milliseconds apart and the difference jumps out:
v4 9f1c8e2a-7b04-4d3e-a1f6-2c9b5d8e4f01
v4 1a55c30d-91ef-4b72-8c04-e7d2a9f6b103
v7 0190f3c1-7a2b-7c00-8e91-3f6d0a4b2c11
v7 0190f3c1-7a41-7d00-9a02-8b5e1c7f9d44
The two v4 values share nothing and sort into random positions. The two v7 values share a leading 0190f3c1-7a… prefix — that's a 48-bit Unix millisecond timestamp, so they sort in creation order as plain text. That single structural difference is the whole reason this debate exists.
Random primary keys hurt in one specific place: the index. When you insert v4 keys into a B-tree (Postgres, MySQL/InnoDB), every insert lands at a random leaf. Pages fragment, the working set of "hot" pages balloons past what fits in cache, and write amplification climbs. Time-ordered keys append to the right edge of the tree instead, so the hot pages stay small. This is why UUID v7, defined in RFC 9562 (which superseded RFC 4122 in 2024), exists at all — it keeps the 128-bit UUID envelope while making the high bits monotonic.
The three, side by side
| Property | UUID v4 | UUID v7 | ULID |
|---|---|---|---|
| Bits of randomness | 122 | 74 | 80 |
| Time-ordered | No | Yes (ms) | Yes (ms) |
| Text form | 36-char hex | 36-char hex | 26-char Crockford base32 |
Fits uuid column type | Yes | Yes | No (store as text/bytes) |
| Standardized | RFC 9562 | RFC 9562 | Community spec, no RFC |
| Library support | Everywhere | Growing, not universal | Good, but per-language |
ULID and v7 solve the same ordering problem; the difference is packaging. ULID gives you a shorter, case-insensitive 01ARZ3NDEK… string that reads well in URLs and logs, at the cost of not being a real UUID — your database's native uuid type rejects it, and anything expecting the 8-4-4-4-12 hex shape breaks.
FAQ
Is UUID v7 safe to expose in URLs?
The random bits (74 of them) are not guessable, so you can't walk the ID space. But the timestamp is right there in the value — anyone can read the creation time down to the millisecond. If that leaks business information (signup order, how many records exist), treat it as a downside. Never use ordering or timestamps for authorization; that's a job for access checks, not opaque-looking strings.
Should I migrate existing v4 keys to v7?
Usually not. Rewriting primary keys means rewriting every foreign key that points at them — high risk for a benign win. Switch new tables to v7 and leave existing v4 keys alone. The index-locality benefit is largest on high-write tables, so target those first if you do change anything.
v7 or ULID for a new service?
If the value ever touches a database uuid column or a client library that validates UUID format, pick v7 — it's the standardized option and drops into existing tooling. Choose ULID only when the shorter, friendlier text form is a feature you'll actually use and nothing in the stack insists on real UUIDs.
Do I still need v1 or v5?
Rarely. v1 (MAC + time) leaked hardware identifiers and has been effectively replaced by v7 for the ordering use case. v5 (namespaced, deterministic from a name) is still the right tool when you need the same input to always produce the same UUID — content-addressing, idempotency keys derived from a natural key.
The decision isn't "which is best" — it's which constraint you're optimizing. Index write pattern points to v7, URL ergonomics point to ULID, and drop-in compatibility with a decade of tooling still points to v4. Pick the axis that actually bites you, generate consistently, and keep authorization independent of whatever the ID looks like. If you just need values to test with, the UUID generator emits v4 and v7 and you can decode either with the UUID validator.
Cover photo by Stanislav Kondratiev on Pexels.
