UUID v4 vs v7 — Differences and When to Use Each
UUID v4 has been the default choice for distributed primary keys for over a decade. Generate a random 128-bit value, format it as eight hex groups, done. No coordination required, practically zero collision probability, works everywhere. So why is UUID v7 — standardized in RFC 9562 in 2024 — spreading fast across production systems in 2026?
The short answer: database performance. UUID v4 destroys B-tree index locality. UUID v7 fixes that while keeping everything else developers love about UUIDs. This article explains what each version actually is, why the difference matters at scale, when to choose each, and how to migrate without downtime.
Need to generate UUIDs right now? Try the Toova UUID generator, which supports both v4 and v7 in bulk. For other random identifiers, random string generator and password generator cover shorter tokens.
UUID v4 — Pure Randomness
UUID version 4 uses a cryptographically secure pseudorandom number generator (CSPRNG) to fill 122 of the 128 bits. The remaining six bits are fixed: four bits encode the version (0100) and two bits encode the variant (10). The result looks like this:
f47ac10b-58cc-4372-a567-0e02b2c3d479
^^^^
version = 4 (random) The randomness is the feature. Two independent systems can generate UUIDs without coordination and never collide — the probability of a collision in a set of 2.71 quadrillion v4 UUIDs is approximately 50%, which means for any practical application the risk is negligible. You do not need a centralized ID server, a database sequence, or a distributed lock.
How v4 is generated
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// => 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
The uuid library (JavaScript), Python's uuid.uuid4(), Go's google/uuid, and every major language runtime delegate to the OS CSPRNG — /dev/urandom on Linux, CryptGenRandom on Windows. The generation cost is effectively zero.
The problem: random insertion kills B-tree indexes
A B-tree index keeps data sorted. When you insert a new row, the database finds where the new key fits in the sorted order and places it there. If every new key is random, it lands at a random position in the index — which means every insert needs to load a different page from disk into the buffer pool. At low volume this is invisible. At high volume (millions of rows, high INSERT rate), it creates a pattern called index fragmentation: the index fills with half-empty pages because each insert hits a different location, and the buffer pool churns constantly because the hot working set spans the entire index rather than a predictable recent slice.
The symptoms in production: INSERT latency increases, autovacuum overhead rises (PostgreSQL), checkpoint pressure grows, and read performance for recent records degrades because "recent" no longer maps to any locality in the index. This is not hypothetical — it is a well-documented pain point on large PostgreSQL and MySQL tables with UUID v4 primary keys.
UUID v7 — Time-Ordered Randomness
UUID v7 was designed explicitly to solve the B-tree fragmentation problem. It encodes a 48-bit Unix millisecond timestamp in the most-significant bits, followed by version bits, 12 random bits, variant bits, and 62 more random bits. Total randomness: 74 bits — still far more than needed to prevent collisions.
018f4e6b-a23c-7d45-9abc-0e02b2c3d479
^^^^^^^^^^^^^^
48-bit Unix timestamp (ms precision)
^
version = 7 Because the timestamp occupies the high bits, UUIDs generated later sort after UUIDs generated earlier. Inserts are always appended to the right edge of the index. The database only needs to keep the most recent index page hot in the buffer pool, not the entire index. At high INSERT rates, this alone can cut write latency by 30–60% and reduce I/O by an order of magnitude on tables with tens of millions of rows.
How v7 is generated
import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
// => '018f4e6b-a23c-7d45-9abc-0e02b2c3d479'
The same uuid library added v7 support in version 10. Python's standard library added it in 3.14. PostgreSQL 17 includes uuidv7() as a built-in function. If you are on an older stack, several small libraries provide v7 generation with no dependencies.
Sub-millisecond monotonicity
What happens when two v7 UUIDs are generated within the same millisecond? RFC 9562 allows implementations to increment a monotonic counter in the random bits to guarantee ordering within the same millisecond. The uuid library does this by default. The result: even if 10,000 IDs are generated in one millisecond, they still sort correctly.
The B-Tree Index Fragmentation Problem in Detail
To understand why this matters, consider what happens at 10,000 inserts per second with a UUID v4 primary key on a table with 100 million existing rows:
- Each insert generates a random 128-bit key that falls at a random position among the 100 million existing entries.
- The database must load the specific B-tree page containing that position into the buffer pool.
- With 100 million rows and 8 KB pages, the index spans roughly 100,000 pages. Each second, 10,000 different pages are needed — far more than a typical 8 GB shared_buffers can hold for just this index.
- Every cache miss results in a disk read. At 10,000 inserts/sec, this can generate thousands of random disk reads per second, saturating even SSDs on large tables.
With UUID v7, all 10,000 inserts per second land at the rightmost leaf page (or a small handful of recent pages). The buffer pool only needs to keep those few pages hot. Cache hit rate for writes approaches 100%. Write amplification drops dramatically.
The same benefit applies to range scans: WHERE created_at BETWEEN x AND y on a v4 table requires a full index scan or a separate timestamp index. On a v7 table, the primary key itself is the timestamp index — the query can seek directly to the right range.
How to Use UUID v7 in PostgreSQL
-- Works natively with UUID type in PostgreSQL 16+
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4 (random)
-- Switch to UUIDv7 via extension or application-side generation
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- With UUIDv7: the timestamp is already embedded, so
-- this separate created_at column is often redundant.
PostgreSQL 17 ships uuidv7() natively. For PostgreSQL 14–16, the pg_uuidv7 extension provides the same function. The UUID data type stores both v4 and v7 identically — 16 bytes, no overhead. The only difference is the bit pattern that determines sort order.
One useful consequence: since the timestamp is embedded, many tables no longer need a separate created_at column for ordering or display. You can extract the timestamp from a v7 UUID with a single function call. This reduces schema complexity and eliminates a write per insert.
UUID v4 vs v7 — Trade-Offs
| Property | UUID v4 | UUID v7 |
|---|---|---|
| Randomness bits | 122 | 74 |
| Sort order | Random | Chronological |
| B-tree INSERT perf | Poor (fragmentation) | Excellent (sequential) |
| Timestamp leakage | None | Yes (ms precision) |
| RFC standard | RFC 4122 (2005), RFC 9562 (2024) | RFC 9562 (2024) |
| Library support | Universal | Growing rapidly (2024–2026) |
| Embedded created_at | No | Yes |
Migration Guide — v4 to v7
Migrating an existing table from UUID v4 to v7 primary keys is a multi-step operation. The key constraint: you cannot change the value of a primary key that is referenced by foreign keys without updating all referencing tables too. Plan for a maintenance window or use a dual-write approach.
-- 1. Add new column
ALTER TABLE orders ADD COLUMN id_v7 UUID;
-- 2. Backfill existing rows (preserve original created_at as the
-- timestamp source; use a UUIDv7 library function in your app)
UPDATE orders SET id_v7 = generate_uuidv7(created_at);
-- 3. Verify no NULLs remain
SELECT COUNT(*) FROM orders WHERE id_v7 IS NULL;
-- 4. Swap columns (requires brief maintenance window)
ALTER TABLE orders ALTER COLUMN id_v7 SET NOT NULL;
ALTER TABLE orders ALTER COLUMN id_v7 SET DEFAULT generate_uuidv7(now());
ALTER TABLE orders RENAME COLUMN id TO id_v4_old;
ALTER TABLE orders RENAME COLUMN id_v7 TO id;
ALTER TABLE orders ADD PRIMARY KEY (id); For zero-downtime migrations on high-traffic tables, the recommended approach is:
- Add the new
id_v7column with a server default that generates v7 UUIDs going forward. - Backfill old rows in batches during low-traffic periods, using the existing
created_attimestamp as the seed for the v7 timestamp portion. - Update all foreign key columns in referencing tables to point to
id_v7. - Rename columns and drop the old primary key constraint during a brief maintenance window.
The backfill step is the longest. At 10,000 rows per batch with a 50 ms sleep between batches, a 100-million-row table takes roughly 8 hours. Start early.
Real-World Impact
Several engineering teams have published benchmarks comparing UUID v4 and v7 on production-sized datasets. The consistent findings:
- INSERT throughput: 2–5x improvement on tables with 50M+ rows when switching from v4 to v7, with the gain increasing as table size grows.
- Write latency p99: Drops from hundreds of milliseconds (v4, under load) to single-digit milliseconds (v7) on the same hardware.
- Index size: B-tree indexes on v7 columns are 15–30% smaller than equivalent v4 indexes on the same data because fragmentation leaves fewer half-empty pages.
- Buffer pool efficiency: Shared buffers hit rate for the primary key index goes from ~40% (v4, large table) to ~99% (v7), because only recent pages need to stay hot.
The gains are negligible below roughly 1 million rows. If your table stays small, stick with v4 for simplicity. Above 10 million rows with any meaningful INSERT rate, v7 is the better default.
When to Use Each
Use UUID v7 when:
- You are designing a new schema and the table will grow large (10M+ rows).
- The table has a high INSERT rate — events, logs, orders, messages, notifications.
- You want a primary key that doubles as a creation timestamp (eliminates a column).
- You are on PostgreSQL 17, MySQL 8.0+, or a modern ORM that supports v7 generation.
- Sorting by ID is semantically equivalent to sorting by creation time — which it will be for most append-heavy tables.
Use UUID v4 when:
- The identifier is exposed to users and must not reveal creation time (invitation codes, share links, billing handles).
- The table is small and stable — no performance benefit justifies the migration cost.
- You are generating IDs in a context where the creation timestamp is sensitive (private user records in a product that competes on growth optics).
- You need something as a one-time credential or token — use a dedicated secret generator, not any UUID version.
Summary
UUID v4 is random, private, and universally supported. UUID v7 is time-ordered, database-friendly, and now standard in major libraries and databases. For new schemas with large or fast-growing tables, v7 is the better default. For IDs exposed to users where timestamp leakage is a concern, v4 remains the right choice.
Generate both versions instantly with the Toova UUID generator — no account required. For shorter tokens, the random string generator produces alphanumeric IDs at any length.