UUID v7 is here. When should you switch from v4?
RFC 9562 standardised UUID v7 in 2024. It's a time-ordered UUID that solves a real performance problem with v4 in databases — but only sometimes. Here's the technical difference, when v7 is worth migrating to, and when v4 is genuinely still the right answer.
If you've ever picked a UUID format for your database, you've probably reflexively typed v4. UUID v4 has been the default for the better part of two decades. It's random, it's collision-resistant by birthday paradox out to ridiculous scales, and it's the version every language's standard library hands you when you call uuid().
The problem with v4 is that it's too random, in a specific way that turns out to matter when you use UUIDs as the clustered primary key on a relational database. UUID v7, standardised in RFC 9562 in 2024, is the fix. It puts a 48-bit Unix millisecond timestamp at the start of the UUID, then fills the rest with random bits. The result is a UUID that's still globally unique but also sorts roughly in creation order — which is what your database's B-tree index has always wanted.
So should you switch? It depends. Here's the analysis.
What v4 looks like, and the index problem
A v4 UUID looks like this:
9f3c8e21-7d4a-4f86-b2c9-1e5a8d7c3b41
The bytes are random except for two: the version marker (the 4 in -4f86-) and the variant marker (the b in -b2c9-). The other 122 bits are pure randomness. That's why v4 is collision-resistant — there are 2^122 possible v4 UUIDs, so you can generate a billion of them per second for a hundred years before you're at a 1% probability of any collision.
Now, suppose you use v4 as the primary key of a busy table. Every insert generates a new UUID that is statistically uncorrelated with the previous ones. Half the time the new key sorts before some existing key; half the time after. The B-tree index that backs your primary key has to split pages, reorganise, and write to random pages in memory and on disk.
For a small table with maybe 100k rows, you'll never notice. For a hot OLTP table doing 5000 inserts/second with billions of rows, this random write pattern is a real performance problem. Your index becomes fragmented. Your buffer pool ratio drops because writes touch pages that aren't in cache. Your disk IOPS budget goes to UUID housekeeping instead of actual queries.
What v7 looks like, and how it fixes this
A v7 UUID looks structurally similar:
019703f6-c50a-7842-9b5c-5e3d4f7a8e1c
The shape is the same. The bytes mean different things. The first 48 bits (the 019703f6-c50a part) are the Unix timestamp in milliseconds. The 4 in -7842- is the version marker (7, hex-encoded). The remaining bits are random.
Because the timestamp leads, a v7 UUID generated at 14:32:01.234 sorts before one generated at 14:32:01.235. Two UUIDs generated within the same millisecond sort in random order, but they sort close together. The B-tree index gets nearly-sequential inserts, just like an auto-incrementing integer ID — except without the central coordination requirement (you can generate v7 UUIDs across machines without colliding).
The performance difference for write-heavy workloads is measurable. For Postgres on a single-server benchmark, switching the primary key from v4 to v7 typically gives 20-40% higher insert throughput and 10-30% smaller index size after a few million rows. For MySQL InnoDB with its strictly clustered primary key, the gain can be even bigger — 2-3x in some pathological cases.
When v7 is worth migrating to
The honest answer: when your table is large enough that the B-tree fragmentation actually matters. That's a higher bar than most teams think.
- Yes, switch: high-volume OLTP tables with hundreds of millions of rows and sustained insert load. Events, audit logs, transaction records, telemetry, message queues backed by a relational store.
- Yes, default to v7 in new projects: there's almost no downside for new code. v7 is at least as good as v4 at uniqueness for any realistic scale, sorts better, and includes a "when was this created" signal for free.
- Probably not worth migrating: tables under 10M rows, low insert rates, application primary keys that aren't the clustered index. The performance win is real but won't pay for the migration risk.
If you're trying to decide whether your specific case qualifies, the simplest test: look at your pg_stat_user_indexes (Postgres) or SHOW INDEX (MySQL) for the table in question. If your primary key index is bigger than expected (a UUID column should be 16 bytes plus B-tree overhead, ~20-25 bytes per row), it's fragmenting. v7 would help.
The "creation time leaks" objection
The most-raised concern about v7: the timestamp in the UUID leaks the creation time of the record. If your UUIDs appear in URLs (like /orders/019703f6-c50a-...), anyone with the URL can derive the order was created at 14:32:01.234 on a specific day.
This is real, but rarely a security issue. The cases where it matters:
- You're using a UUID as a "secret" capability token (a URL someone shouldn't be able to guess). In this case you should never have been using a UUID — use a cryptographically random token from
secrets.token_urlsafe(32). - You're exposing creation timestamps in a way that violates privacy expectations (timing of an account creation, message send time, etc.). If you wouldn't put a
created_atcolumn in the URL, don't put a v7 UUID there either.
For the typical case — an internal-database primary key that occasionally appears in admin URLs — the timestamp leak is meaningless. Anyone with read access to the row could see created_at anyway.
The "clock drift breaks it" objection
If your server's clock jumps backwards (NTP correction, VM time skew, leap second), two consecutive v7 UUIDs can have decreasing timestamps. That violates the "sorts in creation order" promise.
In practice this matters less than it sounds. RFC 9562 lets implementations monotonically increment the random part within a single millisecond, so consecutive v7 UUIDs from the same generator are guaranteed to be monotonically increasing. Cross-machine generation can have small inversions if clocks differ, but the inversion is bounded by clock skew — which on modern infrastructure with NTP is typically <10ms.
If you need strict monotonic ordering across machines, you need something stronger than UUIDs anyway (Snowflake IDs, TSIDs, or coordinated sequences). For "roughly sorted in creation order with no central coordination," v7 is fine.
Migration strategy if you decide to switch
The mistake to avoid: don't try to change existing UUIDs in your database. Migrate forward instead.
- Add a new column. Add a nullable
uuid_v7column to the table. Don't backfill old rows. - Generate v7 on insert. Change application code to write to both
uuid(v4, the old primary key) anduuid_v7on every new insert. - Wait. Let the new rows accumulate. After enough time that the bulk of recent inserts have v7, the B-tree fragmentation problem self-resolves for any range query that's time-windowed.
- Optional: backfill if you must. Some teams backfill historic rows with synthetic v7 UUIDs (using the original
created_atas the timestamp). This makes old data sortable but means the "v7 UUID = real generation time" invariant is now a lie. Don't do this if the timestamp will ever be used as evidence. - Optional: switch the primary key. The biggest migration is making
uuid_v7the clustered primary key. This involves dropping the old PK constraint, creating a new one, and rewriting every foreign key in the database. For a busy production database this is typically a months-long project that you only undertake if the performance pain is severe.
Most teams stop at step 3 — new rows get v7, old rows keep v4, the application uses whichever is available. The B-tree problem fades as the table ages out the v4 era.
Generating v7 in your language of choice
Most major languages now have v7 in their standard or major libraries:
- JavaScript / TypeScript:
crypto.randomUUID()still gives v4. Useuuidv10+ which exportsuuidv7(). Or hand-roll it in 15 lines fromcrypto.getRandomValues— see our UUID generator for a working reference. - Python:
uuid.uuid7()arrived in Python 3.14. For older versions, installuuid7from PyPI. - Go:
github.com/google/uuidv1.6+ hasuuid.NewV7(). - Rust:
uuidcrate with thev7feature flag. - Postgres: built-in
gen_random_uuid()is still v4. For v7, use theuuidv7extension, or generate at the application layer. - MySQL: 9.0+ supports
UUID_v7()as a native function.
The bottom line
UUID v7 is a strict improvement over v4 for the use case of "primary key in a high-volume database." It's the same drop-in semantics, costs nothing extra to generate, gives you a 20-40% throughput win on the workloads that historically suffered most from v4's randomness, and includes a free creation-time signal.
For new projects, default to v7. For existing projects, the migration is only worth doing if the table is genuinely huge and write-heavy. For everything else — config IDs, user IDs in a small SaaS, anything with under a million rows — v4 will continue to be fine for the foreseeable future.
The most-overlooked benefit: a v7 UUID lets you do range queries by time without keeping a separate created_at index. WHERE uuid BETWEEN '019700...' AND '019800...' gives you "all rows created in this hour" using only the primary key. For audit logs and event streams, that's worth migrating for alone.