UUIDs are the default answer to “give every row a globally unique ID without a central coordinator.” But not all UUIDs are equal. The version you pick affects collision risk, index performance, and even what an attacker can infer about your system. Since RFC 9562 formally obsoleted RFC 4122 in 2024, the conversation has shifted: UUIDv7 is now a serious default for database primary keys, and v4 is no longer the automatic choice it used to be.
What a UUID actually is
A UUID is a 128-bit value, typically rendered as 36 characters of hex with dashes:
550e8400-e29b-41d4-a716-446655440000
^^^^ ^^^^
| variant bits
version nibble
Two fields inside that string carry metadata: the version (the 13th hex digit, identifying how the UUID was generated) and the variant (the 17th digit, identifying the layout standard). All standards-conformant UUIDs have a variant starting with 8, 9, a, or b in that position. The remaining 122 bits are where the actual uniqueness lives.
With 122 bits of entropy, collisions are a non-issue at any realistic scale. The choice between v4 and v7 is not about uniqueness — it’s about how the bits are arranged.
UUIDv4: random, collision-proof, index-hostile
UUIDv4 fills its 122 bits with cryptographically random data. The properties:
- Effectively unguessable. A v4 leaks no information about when or where it was created.
- Collision-proof in practice. The probability of a collision among trillions of IDs remains negligible.
- Index-hostile. This is the catch.
The problem is randomness itself. Most databases store rows in a B-tree index ordered by primary key. A B-tree stays efficient when new keys are appended in increasing order — each insert lands at the right edge of the index, and only the final index page needs to be written.
Random UUIDs break this. Each new row lands at a random position inside the index. The insert has to find the right page, often splitting a full page in two to make room. At write volume, this causes:
- Index fragmentation — pages are half-empty and scattered across disk.
- Write amplification — many more pages written per insert than a sequential key would require.
- Cache pressure — the working set of index pages grows because inserts touch random pages instead of concentrating on the hottest few.
For small databases this is invisible. For high-throughput write workloads, it is a real tax.
UUIDv7: timestamp-prefixed and index-friendly
UUIDv7 solves the index problem by front-loading time. Defined in RFC 9562, it lays the 128 bits out as:
- 48 bits — Unix timestamp in milliseconds (the top bits)
- 4 bits — version marker (
7) - 12 bits — a counter or random “sub-second” field (lets you order IDs generated within the same millisecond)
- 2 bits — variant marker
- 62 bits — random data
Because the most significant bits are the timestamp, UUIDv7s generated in sequence are lexicographically sortable. New IDs almost always sort after all existing IDs, so inserts land at the right edge of the B-tree — exactly where the database wants them. Page splits drop, write amplification drops, and the index stays dense.
A useful side effect: an unordered list of UUIDv7s can be sorted as strings and it will come out in chronological order. That is handy for anything that wants time-ordering without a separate created_at column.
When to still prefer v4
UUIDv7 is not strictly better. Its timestamp prefix leaks information:
- Creation timing is visible. Anyone who sees a v7 can extract the millisecond it was generated. If you expose IDs in URLs or APIs, that leaks when records were created — sometimes that’s sensitive.
- Predictability for an attacker. The top 48 bits of a v7 are predictable from the wall clock. The remaining 74 bits of randomness still make guessing impractical, but the structure is no longer opaque.
Reach for v4 when you specifically want opaque IDs — for example, session tokens, share links, or any identifier where revealing creation order would be a problem.
Storage: use the native 128-bit type
The 36-character string representation is for humans, not databases. Storing a UUID as VARCHAR(36) wastes space and breaks index ordering semantics (string comparison is slower and follows different rules than binary comparison).
Use the native type:
- PostgreSQL:
uuid(16 bytes). PostgreSQL 18 added a built-inuuidv7()function. - MySQL:
BINARY(16), or store asBIT(128). - SQL Server:
UNIQUEIDENTIFIER(16 bytes).
Stored natively, the comparison and hashing used by indexes is as fast as it gets for a 128-bit key.
Generating UUIDs securely
For UUIDv4 in the browser or Node, use the Web Crypto API:
const id = crypto.randomUUID(); // UUIDv4, cryptographically random
crypto.randomUUID() is available in all modern browsers and in Node 19+. It uses the platform CSPRNG, so the output is suitable for security-sensitive uses — unlike Math.random()-based generators, which are predictable and should never be used for IDs that face untrusted callers.
For UUIDv7, the browser does not ship a native generator yet. Use a vetted library that reads the timestamp and fills the random bits from crypto.getRandomValues(). Whatever you pick, confirm the random portion comes from a CSPRNG — the timestamp half is public by design, but the random half must be unpredictable.
The short version
- UUIDv4 — fully random. Best when you want opaque IDs and don’t care about index locality.
- UUIDv7 — timestamp-prefixed. Best as a database primary key for write-heavy tables; sorts naturally and keeps indexes dense.
- Store as a native 128-bit type, never
VARCHAR(36). - Use
crypto.randomUUID()for secure v4 generation in the browser or Node.
If you want to see what each version actually looks like, or generate a batch for testing, a UUID generator will produce v4 and v7 side by side so you can compare the structure directly.