Why I Migrated to UUID v7 for New Databases

By Λ · May 18, 2026 · 7 min read

Postgres got native UUID v7 support in version 18. MySQL added a v7 function in 8.4. SQL Server has had it since 2025. Java's standard library, .NET, Go, Rust, and Python all now ship v7 generators in the core stdlib or first-party libraries. RFC 9562 (2024) formally specified it. The case for using v7 in new code is now overwhelming. This post is the case for it, with the caveats.

The performance problem v7 solves

UUID v4 is random. That is its only virtue. Every v4 is uniformly distributed across the 122-bit random space.

If you use v4 as a primary key in a database with a B-tree clustered index (Postgres, MySQL InnoDB, SQL Server clustered indexes), every insert lands in a random leaf of the tree. The B-tree fragments. The cache hit rate on the index drops. Inserts that should be O(log n) start behaving like O(n) for the I/O.

The measurements vary by workload and hardware, but the consistent finding across many benchmarks is: v4 inserts on a B-tree table above 10 million rows are 3-5x slower than the equivalent v7 inserts. On NVMe storage the gap is closer to 3x; on spinning disks it can be 10x.

v7 fixes this by putting a millisecond timestamp in the first 48 bits. Sequential inserts cluster at the right edge of the B-tree. Cache locality returns. The index does not fragment.

The v7 byte layout

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           unix_ts_ms                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Reading from the top: 48 bits of millisecond Unix timestamp, 4 bits of version (0111 = 7), 12 bits of random, 2 bits of variant (10), 62 bits of random. Total 128 bits = 16 bytes = the same size as v4.

The timestamp is in network byte order (big-endian), so lexicographic sort order matches chronological order. A v7 UUID generated at noon sorts before one generated at 1pm without parsing.

What the timestamp leaks

This is the trade-off. A v7 UUID reveals approximately when the row was created. For internal database keys this is fine. For URL-visible IDs (account pages, share links, password reset tokens), it leaks information.

Concrete attack surface: if you publish /users/{uuid_v7} as a URL and your UUIDs leak creation timestamps, an attacker can enumerate users by creation order, guess account ages, and correlate with public events ("the breach happened at X, which users were created near that time").

The fix: do not put v7 UUIDs in public URLs. Use a NanoID, a hash of the UUID, or a separate "public_id" column. Use v7 for internal database keys; use v4 or a NanoID for anything public-facing.

Migrating an existing database

You usually do not migrate. Existing v4 keys stay v4 forever; they would not magically perform better if rewritten. v7 is for new tables and new records.

If you really must convert a v4-keyed table to v7 (for example, you discovered that key performance is your bottleneck), the path is:

  1. Add a new id_v7 UUID column.
  2. Populate it with v7 UUIDs derived from the row's created_at timestamp.
  3. Update all foreign keys to point at id_v7.
  4. Swap the primary key from id_v4 to id_v7.
  5. Drop the old id_v4 column once nothing references it.

This is risky and most teams will not do it. The simpler answer: keep v4 for legacy tables, use v7 for new tables.

UUIDv7 in code

Native support exists in most languages by 2026:

// Postgres 18+
INSERT INTO events (id, ...) VALUES (uuidv7(), ...);

// JavaScript (RFC 9562 reference impl)
const id = crypto.randomUUID();   // still v4 in 2026; check stdlib for v7
// or use uuid package:
import { v7 } from 'uuid';
const id = v7();

// Python 3.13+
import uuid
new_id = uuid.uuid7()

// Go (since Go 1.23)
id := uuid.New7()

// Rust (uuid crate v1.10+)
let id = Uuid::now_v7();

If your language does not have native v7 yet, the implementation fits in 20 lines. See the UUID generator's source for a minimal JS implementation.

Common questions

Should I use v7 in distributed systems?

Yes, with caveats. The 48-bit timestamp has millisecond resolution, so two machines generating IDs at the same millisecond can produce IDs that sort within the same window. Within a millisecond, the 74 random bits provide ordering. For most workloads this is fine. If you need strict total ordering across machines, use a Snowflake-style ID or coordinate via a service.

What about clock skew?

If two machines have clocks off by minutes, their v7 UUIDs will sort by clock-time, not by generation order. NTP keeps most servers within ~10ms of true time which is acceptable. Cloud VMs almost always run NTP. Bare metal in odd environments sometimes drifts; check.

What about UUIDv6?

v6 was a 2022 proposal that reorders the v1 timestamp to put the high bits first. v7 superseded it before v6 saw real adoption. Skip v6 unless you have a specific reason.

What about ULID, KSUID, Snowflake?

All pre-RFC alternatives to v7. Most were trying to solve the same problem. v7 was the standards process unifying them. New code should use v7 unless you have an existing ULID/KSUID/Snowflake ecosystem you need to interop with.

Quick recommendation

Related