Skip to main content

UUIDv7 Explained: The Time-Ordered UUID Your Database Index Will Thank You For

Why UUID version 7 starts with a timestamp, how that makes it sortable and index-friendly for primary keys, and when to pick v7 over random v4.

Published By Li Lei
#uuid #uuidv7 #database #primary-keys #developer-tools

UUIDv7 Explained: The Time-Ordered UUID Your Database Index Will Thank You For

For years the default identifier for a new database row was either an auto-increment integer or a random UUIDv4. Integers leak row counts and are awkward across distributed systems. Random v4 fixed the leaking but quietly handed you a performance bill that only showed up once the table got big. UUID version 7, standardized in RFC 9562 in May 2024, is the answer to that bill: a UUID that keeps the uniqueness of v4 but puts a real timestamp at the front so the IDs sort by creation time.

This post walks through what that timestamp prefix actually buys you, shows the exact bytes of a v7 value, and lays out when v7 is the right call versus when plain v4 is still fine.

What a UUIDv7 Actually Contains

A UUIDv7 is still a 128-bit value in the familiar 8-4-4-4-12 hex shape, so it drops into any UUID column or library with zero changes. What differs is the layout of those bits. The leading 48 bits are a Unix millisecond timestamp, big-endian. After that comes a four-bit version nibble (the value 7), some random bits, the two variant bits, and a final stretch of randomness.

The concrete payoff: because the most significant bits are a timestamp, two v7 values compare in time order as plain strings. An ID minted at 9:00:00.000 always sorts before one minted at 9:00:00.001, with no extra column and no custom comparator. A random v4 gives you no such guarantee. Two v4 values created a millisecond apart are as likely to sort in one order as the other, because every bit is noise.

Reading the Timestamp Out of a v7

The timestamp is not hidden or hashed. It is the first 12 hex characters, sitting in plain sight. Take this v7:

0190b3b0-6400-7000-8000-000000000000

Strip the hyphens and the first 12 hex digits are 0190b3b06400. Read as a big-endian integer that is 1721001600000, a count of milliseconds since the Unix epoch. Convert that and you land on 2024-07-15 00:00:00 UTC. The character right after, the 13th hex digit, is 7 — that is the version marker, and a correct validator checks it before trusting the date.

You can do this round trip yourself in the UUIDv7 Generator: generate a value, paste it back into the decoder, and watch it report the same millisecond count plus a readable UTC and local time. If you want to double-check a raw epoch number against a wall-clock time, the timestamp converter does the millisecond-to-date math on its own.

Why the Timestamp Prefix Is Index-Friendly

This is the part that matters for production tables, and it comes down to how a B-tree index stores keys.

A B-tree keeps keys in sorted order across pages. When you insert a random v4, its key could belong anywhere in that sorted space, so each insert dirties a different, often cold, page. On a high-write table that means constant page splits, an index that fragments fast, and a working set that refuses to stay in memory because every insert touches somewhere new.

A v7 inverts that pattern. Since fresh IDs lead with the current timestamp, they are numerically close to the IDs you just generated. Inserts cluster at the right edge of the index, the same few pages stay hot in cache, and page splits drop sharply. You get the locality of an auto-increment key without the centralized counter that breaks in a distributed setup.

There is a second, free benefit: ORDER BY id is also ORDER BY creation time. You can drop the separate created_at column you were maintaining purely for sorting, and "give me the 50 newest rows" becomes ORDER BY id DESC LIMIT 50. This is not a niche trick — Postgres 18 and SQL Server now ship native v7-style generators precisely because the index behavior is so much better than random UUIDs.

A Note From Building With Them

When I first swapped a v4 primary key for v7 on a write-heavy outbox table, I went in skeptical that a change to the ID format could move anything I'd notice. I generated a few values, eyeballed them, and confirmed they really did sort by time as strings. What surprised me afterward was not raw throughput but how much steadier the index stayed: the bloat that used to creep in between vacuum runs flattened out, because inserts stopped scattering across the whole tree. The lesson stuck — the win from v7 is less a benchmark number and more the absence of a slow problem you used to schedule around.

v7 or v4: How to Choose

The two versions are not competitors so much as tools for different jobs.

Reach for v7 when:

  • The ID is a database primary key, especially on a table that takes a lot of inserts.
  • You want rows to carry their own creation order, so sorting and "recent first" queries come for free.
  • You are building an append-only log, an outbox, or an event stream where ordering is the whole point. Turn on monotonic mode so even thousands of IDs minted in the same millisecond stay strictly ascending.
  • You want a standards-blessed identifier that any UUID-aware framework accepts without conversion.

Stay with v4 when:

  • The value must be unguessable in full — a session token, a password-reset code, an unsubscribe link. A v7 leaks its creation time by design, so it is an identifier, not a secret.
  • You explicitly do not want creation timing exposed in a public URL. Adjacent monotonic v7s even hint at how fast you create records.
  • You just need an opaque random handle and index locality is irrelevant.

If you are comparing v7 against the broader field of sortable IDs, a ULID carries the same 48-bit-timestamp-then-randomness idea but encodes as a shorter, hyphen-free Base32 string that reads nicer in a URL. The trade is that v7 is a real UUID and slots into a UUID column directly, while ULID needs a string column or a conversion step.

One Validation Gotcha

A common mistake undoes the whole benefit: validating a v7 with a generic UUID regex that only checks the 8-4-4-4-12 shape. Such a pattern happily accepts a v4, or even the all-zero NIL UUID, and if you then try to read a timestamp out of those you get garbage. The fix is cheap — confirm the version digit (the 13th hex character) is 7 and the variant bits are 0b10 before trusting the embedded time. The decoder on the generator page does exactly this check, rejecting a v4 or a mistyped string instead of inventing a date.

UUIDv7 is one of those rare changes that costs nothing to adopt — same column type, same libraries, same 36-character string — yet quietly fixes a real index problem and hands you free time-ordering on top. For new primary keys on write-heavy tables, it has become the sensible default.


Made by Toolora · Updated 2026-06-13