Quick decision
| I need… | Use |
|---|---|
| A database primary key | v7 — time-ordered, index-friendly |
| A URL to share with users | v4 — no timestamp, no leakage |
| An event or log correlation ID | v7 — creation time is useful |
| A password-reset or invite token | v4 — opaque, nothing inferred |
| Both storage and public exposure | v7 primary key + v4 public handle |
The short answer
Use UUID v7 for anything you control the storage of — database primary keys, event IDs, log correlation IDs, message keys. Use UUID v4 for anything a user or third party sees where creation time or creation rate is sensitive — share links, password-reset tokens, invitation codes, public object IDs.
Both are 128 bits, both are defined by RFC 9562, and both have collision probabilities low enough to ignore in practice. The decision is not about uniqueness. It is about whether embedding a readable timestamp in the identifier helps you or hurts you.
What actually differs: the bits
A v4 UUID is 122 bits of random data, with 4 bits fixed as the version and 2 as the variant. Nothing about it is derived from the machine, the clock, or the sequence of previous values.
A v7 UUID spends its first 48 bits on a big-endian Unix millisecond timestamp. The version and variant nibbles sit in the same positions as v4, and the remaining 74 bits carry randomness — with the option, taken by this site’s generator, of using 12 of them as a per-millisecond counter so that values minted inside the same millisecond still sort correctly (RFC 9562 §6.2, Method 1).
Because the timestamp occupies the most significant bits and UUIDs are written as big-endian hex, lexicographic string order matches chronological order for v7. Sorting v7 UUIDs as text, as bytes, or as native uuid values all give the same sequence. That single property is the source of every practical difference below.
Why the difference shows up in a database
Most relational databases store the primary key in a B-tree. Where a new key lands in that tree determines how much work the insert costs.
With v4, every insert targets a uniformly random position in the key space. As the table outgrows memory, each insert tends to touch a different leaf page, and each of those pages must be read in, modified, and written back. Pages fill unevenly and split, the tree carries more free space than it needs, and the working set that has to stay cached to keep inserts fast is effectively the whole index.
With v7, consecutive inserts share a timestamp prefix, so they land in the same handful of leaf pages. Those pages stay hot in cache, they fill up before the insert point moves on, and the index behaves much like one keyed on an auto-incrementing integer — without needing a central sequence, and while still being safe to generate on any client.
The trade-offs that come with that:
- Range scans by time become possible.
WHERE id > :some_v7is a chronological filter, and an index on the primary key can serve “most recent N” queries without a separatecreated_atindex. - Insert hotspots move to one place. In a distributed database that partitions by key range, every writer targeting the same key range can concentrate load on one shard. Hash partitioning, or a shard prefix ahead of the UUID, avoids this. For single-primary PostgreSQL or MySQL it is not a concern.
- The clock now matters. v7 depends on the system clock being roughly correct. A clock that jumps backwards produces values that sort before ones already written. RFC 9562 §6.1 discusses handling this; in practice, run NTP.
PostgreSQL 18 ships a native uuidv7() function. Earlier versions can store v7 values generated anywhere — the type is just 128 bits.
When v4 is the right answer
The timestamp that makes v7 useful internally is a disclosure when the identifier is public:
- It reveals creation time. A v7 UUID in a URL tells anyone who receives it exactly when the underlying record was made, to the millisecond.
- It reveals volume. Two v7 UUIDs collected an hour apart bound how many identifiers were minted between them if the generator uses a counter. That can expose sign-up rates, order volume, or internal growth.
- It invites enumeration attempts. v7 is not guessable — the low bits are still random — but a time-ordered ID makes it obvious to an attacker that neighbouring records exist and are worth probing for weak authorisation.
None of these are reasons to avoid v7 in your own tables. They are reasons to keep the public-facing identifier separate. A common pattern is a v7 primary key for storage plus a v4 (or another opaque token) as the externally shared handle.
Use v4 outright for password-reset links, invitation codes, unsubscribe links, and API keys — with the caveat that a UUID is an identifier, not a secret. It has no authentication properties on its own, and 122 bits of entropy is a floor, not an authorisation check.
Migrating from v4 to v7
Because both versions share a type and a format, the change is usually confined to the code that mints new values:
- Switch the generator. Point new inserts at a v7 implementation. Note that JavaScript’s built-in
crypto.randomUUID()produces v4 only — v7 needs a library or a short implementation of your own. - Leave existing rows alone. Old v4 values stay valid and keep resolving. The column accepts both.
- Do not sort mixed data by ID and expect chronology. Rows written before the switch have random keys; only the v7 ones carry order. Keep using
created_atfor anything spanning the cutover. - Rebuild the index if the table is large and old. An index fragmented by years of random inserts does not repack itself;
REINDEXreclaims that space once new inserts are sequential.
There is no need for a backfill. Rewriting historical IDs means rewriting every foreign key that references them, which is a large amount of risk in exchange for a cosmetic gain.
Choosing, in one pass
Ask whether the identifier is stored or shown:
- Stored, internal, indexed → v7. The index behaviour is free and the timestamp is useful.
- Shown to users, sent in links, handed to third parties → v4, or a separate opaque token.
- Both → v7 primary key, v4 public handle. Two columns is cheaper than picking wrong.
If you are still unsure, v7 is the safer default for new work in 2026: the failure mode of v4 (index fragmentation) grows silently with table size and is awkward to fix later, while the failure mode of v7 (timestamp disclosure) is visible at design time and is solved by not putting the key in a URL.
When to use which
| Use case | Recommended | Why |
|---|---|---|
| Database primary key | v7 | Time-ordered inserts keep B-tree indexes healthy |
| Session tokens, share links, reset URLs | v4 | Creation time must not leak; pure randomness |
| Event / log correlation IDs | v7 | Natural time sort aids debugging |
| Public-facing opaque IDs | v4 | No ordering information exposed |
| Offline / distributed generation | either | Both need no central coordinator |
Frequently asked questions
Should I use v4 or v7?
Use v7 for database primary keys (time-sortable, index-friendly) and v4 for anything where creation order could leak information, like tokens or share links.
Can I use UUID v7 in PostgreSQL?
Yes. PostgreSQL 18 ships a native uuidv7() function; earlier versions can store any UUID this tool generates in a uuid column.
Is UUID v7 better than UUID v4?
Neither is strictly better. v7 is the better default for database primary keys because its time-ordered prefix keeps B-tree inserts localised. v4 is the better choice for anything externally visible where the creation time or the rate of creation should stay private.
Does UUID v7 leak information?
Yes — the first 48 bits are a Unix millisecond timestamp, so anyone holding a v7 UUID can read when it was created to the millisecond. Comparing two v7 UUIDs also reveals how far apart the records were created, which can expose sign-up or transaction volume.
Can I mix UUID v4 and UUID v7 in the same column?
Yes. Both are 128-bit values with the same textual format, so a uuid column stores either. The version nibble tells them apart, and existing v4 rows keep working after you switch new inserts to v7.