UUID v4 vs v7: Which Should You Use?
UUID v4 vs v7 for database primary keys: see why v7 improves insert performance, when v4 protects privacy, and how to generate both.
For a new database primary key in 2026, default to UUID v7 and reach for v4 only when the identifier is public and its creation time must stay private.
If you have ever watched insert latency creep up on a busy table and traced it back to a primary key that lands in a different corner of the index every time, this question will feel familiar. The fix turns out to be a lot smaller than a schema migration. Both formats are 128-bit UUIDs standardized in the same RFC, so the choice is not about compatibility or collision safety. It comes down to one property: whether your IDs sort in creation order. v7’s time-ordering fixes the index fragmentation that random v4 keys cause on write-heavy tables, at the cost of embedding a readable timestamp in every value. This guide breaks down the structural difference, the database-index mechanism behind v7’s write performance, the privacy trade-off, current library support, and a decisive default.
Key Takeaways
- UUID v4 and v7 are both 128-bit, 16-byte identifiers standardized in RFC 9562 (May 2024); v7 replaces the leading 48 bits with a Unix millisecond timestamp so its values sort in creation order, while v4 is fully random.
- Random v4 keys scatter across a B-tree index and cause page splits, cache churn, and fragmentation; v7’s time-ordered prefix makes inserts append near the end of the index, behaving much more like a sequential key.
- A v7 ID leaks its own creation time to millisecond precision; a v4 ID does not. Use v4 for public identifiers, invite links, or anywhere your growth rate must stay private.
- v7 support is first-class: PostgreSQL 18 ships native
uuidv7(), Python 3.14 addeduuid.uuid7(), and the JavaScriptuuidpackage (v14.x) exportsv7(). - No UUID is a secret: use a dedicated 256-bit random token for authentication and reserve UUIDs for identity.
What’s the difference between UUID v4 and v7?
UUID v4 and v7 are both 128-bit, 16-byte identifiers standardized in RFC 9562, published May 2024 as a Standards-Track document that obsoletes the older RFC 4122. The only structural difference is where the bits come from. UUID v4 is 122 bits of randomness with 6 bits reserved for version and variant markers, which makes it fully random and unordered. UUID v7 replaces the leading 48 bits with a Unix timestamp in milliseconds and fills the remaining ~74 bits with randomness (plus the version and variant markers), so v7 values sort in creation order lexically and byte-wise while v4 values do not.
UUID v4: [ 122 random bits ...................... ] + version/variant
UUID v7: [ 48-bit ms timestamp ][ ~74 random bits ] + version/variant
That single change is the entire decision. Both keep the same collision math from the randomness portion, both fit in the same column, and both are covered by the same standard.
Discover how at OpenReplay.com.
Why does UUID v7 win for database keys?
v7 outperforms v4 as a primary key because its timestamp prefix gives inserts index locality that random keys destroy. Random v4 keys land at random positions in a B-tree index, causing page splits, cache churn, and fragmentation as every insert targets a different part of the tree. This is the exact problem RFC 9562 lists as its reason for defining v7: when identifiers carry no time ordering, each new row has to be written wherever its random value happens to fall, whereas values generated one after another under a time-ordered scheme end up as neighbours in the index. v7’s monotonic prefix means new rows append near the end of the index, so page splits become rare and insert throughput approaches that of a sequential integer key, while keeping UUID collision safety and distributed generation.
The penalty is worst on clustered-index engines. In MySQL InnoDB and SQL Server, the table is physically ordered by the primary key, so random inserts rewrite pages throughout the structure. PostgreSQL stores rows in a heap with separate indexes, which is less sensitive to key randomness, but its indexes still lose cache locality with random v4 keys. The magnitude varies by engine, workload, and hardware, so treat specific percentages from unsourced blog benchmarks skeptically and measure your own table. The mechanism itself is not in dispute.
Time-ordering also holds under burst inserts. Implementations add a sub-millisecond counter so IDs generated in the same millisecond still sort correctly: Python’s uuid.uuid7() sets aside 42 bits as a counter so values minted inside a single millisecond keep their order, and PostgreSQL 18’s uuidv7() builds each value from a millisecond Unix timestamp, a sub-millisecond fraction, and random bits.
When UUID v4 Is Still the Right Call
Choose v4 when the ID is exposed and its creation time is sensitive, because a v7 ID embeds its own creation time to millisecond precision. Anyone who can read the ID can read when the record was made, which makes v7 a poor fit for public-facing identifiers. The Aiven team reaches the same conclusion: once a primary key is handed to end users through an external app or API, v7 stops being a sensible choice, because the identifier gives away when the record was created. Use v4 for invite tokens, share links, or anywhere a competitor could infer your growth rate from ID ranges.
One caveat applies to both versions: no UUID is a security token. The non-random bits in v7 are predictable, and v4’s randomness quality is implementation-dependent, so neither should gate authentication. Generate a dedicated cryptographically random string, at least 256 bits, for secrets, and use UUIDs only for identity.
Generating v7 Today, and Migrating Incrementally
v7 support is now broad across databases and languages, though the exact version matters:
| Platform | v7 generation | Version |
|---|---|---|
| PostgreSQL | uuidv7() (native) | PostgreSQL 18 |
| Python | uuid.uuid7() (stdlib) | Python 3.14 |
| JavaScript / Node | v7() from uuid | uuid v14.x |
PostgreSQL 18 shipped uuidv7() as a native function and added a uuidv4() alias for the existing gen_random_uuid(); on PostgreSQL 17 and earlier you need an extension or app-side library. Python added uuid.uuid7() to the standard library in 3.14, not 3.12, where the call raises AttributeError. In JavaScript, the uuid package exposes v7 via an ESM named import, and per its changelog the package dropped CommonJS support in v12:
// Node.js, uuid v14.x
import { v7 as uuidv7 } from "uuid";
const id = uuidv7();
-- PostgreSQL 18
SELECT uuidv7();
If you want to see the difference before committing to a column type, generate a batch of each and line them up. OpenReplay’s UUID generator produces v4 or v7 values in the browser, up to 500 at a time, with toggles for uppercase, hyphen-free, and quoted output so you can paste them straight into SQL or a JSON fixture. Sort a column of v7 values and they come out in creation order; do the same with v4 and they scatter. The values come from the Web Crypto API in your own tab, so none of them are sent anywhere.
Migration needs no rewrite. v4 and v7 share the same 16-byte uuid column type, so you can keep existing v4 rows and generate new rows as v7 in the same table. The version is encoded in the value, and no backfill is required. New inserts cluster at the end of the index, and fragmentation eases gradually as pages are rewritten over time; this is a progressive effect, not an instant defragment.
The Verdict: Which Should You Use?
Default to UUID v7 for new database primary keys, logs, and event streams; choose v4 when unpredictability matters. v7 gives you the write performance of a sequential key with the collision safety and distributed generation of a UUID, and it now has native support in the platforms most teams already run. Reserve v4 for identifiers that are public and where creation time must stay private.
Two alternatives round out the decision. ULID encodes the same timestamp-plus-random idea in Crockford base32, giving a shorter 26-character URL-friendly string, but it is not an IETF standard and lacks a native uuid column type. Plain bigint auto-increment remains the smallest and fastest option for a small, single-node system that will never need distributed ID generation.
If you’re standing up a new table today and feeling index pain from v4 keys, switch new inserts to v7, leave your old rows in place, and let the index settle. The win is available with almost no migration cost.
FAQs
Can you extract the creation timestamp from a UUID v7 value?
Yes. Because UUID v7 stores a 48-bit Unix millisecond timestamp in its leading bits, you can decode when the value was generated. PostgreSQL 18 exposes uuid_extract_timestamp() for exactly this, and it was extended to support version 7 values. This is a feature for debugging and time-range queries, but also the reason v7 leaks creation time and should not be used for public identifiers where that timing is sensitive.
Do UUID v7 and v4 have the same collision risk?
No, but the difference is negligible in practice. UUID v4 carries 122 random bits while v7 keeps roughly 74 random bits after reserving 48 bits for the timestamp plus version and variant markers. v7 has fewer random bits, yet collisions are only possible between IDs generated in the same millisecond, and implementations add a monotonic counter within that window. For real workloads both are effectively collision-free at any realistic generation rate.
Does UUID v7 improve read query performance, or only inserts?
v7 primarily helps write-side performance and range scans by ordering rows near each other in the index, which reduces page splits and improves cache locality. Do not attribute large read speedups to v7 alone. PostgreSQL 18's official 'up to 3x' read improvement comes from its new asynchronous I/O subsystem, a feature independent of uuidv7(). The accurate scope for v7's performance benefit is index locality and insert throughput.
Should I migrate existing UUID v4 primary keys to v7?
Usually no full migration is needed. v4 and v7 share the same 16-byte uuid column type and the version is encoded in the value, so you can leave existing v4 rows untouched and generate new rows as v7 in the same table with no backfill. New inserts cluster at the end of the index and fragmentation eases gradually as pages are rewritten. A rewrite of old rows is only worth it if fragmentation is already causing measured pain.