Skip to content
UUID Generator
Blog 01

UUID v4 vs v7: which one belongs in your primary key

Updated October 12, 2026 · Reading time: 7 min · 7 sections

Pick v7 for the primary key of any table that keeps growing, and keep v4 for identifiers whose creation time is nobody's business. That is my default in the UUID v4 vs v7 decision; what follows is the reasoning, so you can tell when your table is the exception.

What sits inside the 128 bits

Both versions share the same frame: 128 bits written as 32 hex digits in 8-4-4-4-12 groups, a version nibble in the 13th hex digit and the variant bits at the top of the 17th. Everything else is where they part ways.

  • v4 fills the remaining 122 bits from a cryptographically secure random generator. No field means anything.
  • v7 starts with a 48-bit Unix timestamp in milliseconds, then the version, 12 bits called rand_a, the variant, and 62 bits called rand_b. RFC 9562 lets a generator spend part of rand_a and rand_b on a counter, which is what keeps a batch in order.
Bit layout of UUID v4 and UUID v7: v4 is random apart from version and variant, v7 begins with a 48-bit millisecond timestamp followed by a 42-bit counter and 32 random bits
Same frame, different contents: v7 puts the time first, so the bytes sort by creation time.

Because the timestamp sits in the most significant bits, big-endian, a v7 sorts by creation time when you compare it as text, as bytes or as a native uuid value in PostgreSQL. A v4 sorts by nothing.

Why random keys hurt a B-tree index

A primary key index is a B-tree ordered by key. With v7, each new key is larger than the previous one, so it lands on the rightmost leaf page; when that page fills, the database opens a new one and the old page stays full. With v4, each new key lands on a random leaf somewhere in the index. RFC 9562 §6.13 makes the same point: time-ordered values give better index locality because new keys end up next to each other.

Random placement costs you in three ways on every insert-heavy table:

  • Page splits. A full page in the middle of the index splits in two to make room. Random insertion leaves leaf pages about 69 % full on average (the classic ln 2 result for random B-tree inserts), so the same index needs more pages and more disk.
  • Working set. Every insert touches an arbitrary leaf, so the whole index has to stay in memory to keep inserts fast. With v7 only the right edge is hot. Once a v4 index outgrows the buffer cache, inserts start waiting on disk reads.
  • Write amplification. In PostgreSQL the first change to a page after a checkpoint writes a full page image to the WAL. Scattered inserts dirty many pages per checkpoint; ordered inserts dirty a handful.

Indexes on foreign keys inherit the pattern. A child table indexed on order_id gets random inserts too when the parent key is v4.

None of this shows up while the table and its indexes fit in RAM. It starts when the index no longer fits, and then it is the first thing I look at when inserts slow down with no code change.

The counter that keeps v7 in order

A millisecond is a long time for a server. Generate a thousand IDs in a loop and many share a timestamp. If the remaining bits are pure random, IDs from the same millisecond come out in random order, and a "sorted" bulk insert is not sorted. The old version of the generator on this site had exactly that bug.

RFC 9562 §6.2 offers several fixes. The one I prefer is Method 1, a fixed-length dedicated counter:

  1. On a new millisecond, seed the counter with random bits, leaving its top bit at zero so there is room to count up.
  2. Within the same millisecond, add 1 to the counter instead of drawing new random bits.
  3. If the counter overflows, move the timestamp forward by one millisecond and reseed.
  4. If the system clock goes backwards, keep the last timestamp so the sequence never decreases.

Python 3.14's uuid.uuid7() uses a 42-bit counter this way, and so does the UUID generator here: 12 bits of rand_a plus the top 30 bits of rand_b, with the last 32 bits still random. A 41-bit random seed leaves at least 2,199,023,255,552 increments before an overflow, far more than one process creates in a millisecond. The Bulk tab makes up to 10,000 v7 IDs at once and says when the list is strictly ascending; paste any list into the Validate & decode tab and it checks the order for you.

The counter orders IDs from one generator. Two application servers still interleave inside the same millisecond, which is harmless for the index: all of those keys land on the same rightmost pages.

What a v7 key tells anyone who sees it

The first 12 hex digits of a v7 are a timestamp in plain sight. Take the RFC test vector 017f22e2-79b0-7cc3-98c4-dc0c0c07398f: the hex 017f22e279b0 is 1,645,557,742,000 milliseconds since the Unix epoch. No key, no secret, no special software. The Validate & decode tab prints that instant in UTC and in your local time, and any library can do the same.

So a v7 order ID in a URL tells the customer when the order was placed, v7 user IDs tell a scraper when each account signed up, and any two IDs reveal the time between two events. Whether that matters is a product question, not a database one. RFC 9562 §8 covers the other half: UUIDs are identifiers, not secrets, so neither version should stand in for a session token or a password-reset key.

When v4 is still the right call

  • IDs you show to the outside world where creation time is sensitive: user IDs in public profile URLs, document IDs in shared links.
  • Small or read-mostly tables. A lookup table with a few thousand rows fits in memory either way.
  • Writes you want spread out. Stores that partition by key range send every new time-ordered key to the same partition; a random key spreads the load across shards.
  • Databases that sort the key differently. SQL Server orders uniqueidentifier by its last group first, so a v7 there does not give you append-only inserts. The UUID vs GUID guide explains that quirk.

A compromise I use often: v7 as the internal primary key, plus a separate v4 column with a unique index as the public identifier. Collisions are not a reason to prefer either. With 122 random bits, a billion v4 IDs have a collision probability of about 9.4 × 10⁻²⁰, and v7 adds the timestamp on top of its random bits.

UUID v4 vs v7 side by side

UUID v4 vs v7 for database keys
UUID v4UUID v7
Contents122 random bits48-bit ms timestamp, then 74 bits of counter and random data
Sort orderNoneCreation time, to the millisecond
B-tree insertsRandom leaf pages, splits everywhereRightmost leaf pages, close to append-only
What it revealsNothingWhen it was generated
Built-in generatorscrypto.randomUUID(), gen_random_uuid(), uuid.uuid4(), Guid.NewGuid()uuidv7() in PostgreSQL 18, uuid.uuid7() in Python 3.14, Guid.CreateVersion7() in .NET 9
Best forPublic IDs, small tables, writes spread across shardsPrimary keys of growing tables, event and log tables, paging by time

Moving an existing table from v4 to v7

  1. Keep the column type. A v7 is a valid UUID, so a uuid column in PostgreSQL, BINARY(16) in MySQL or uniqueidentifier in SQL Server holds both versions side by side.
  2. Change only where new keys come from: the column default (ALTER TABLE orders ALTER COLUMN id SET DEFAULT uuidv7(); in PostgreSQL 18) or the call in your application. The guide on generating a UUID in code lists the call for each language.
  3. Leave old keys alone. Rewriting primary keys means rewriting every foreign key, cache entry and URL that was ever shared. Old v4 rows stay scattered, new rows append on the right, and the index improves as new rows outnumber old ones.
  4. Audit code that leaned on unpredictability. Anything that relied on IDs being hard to guess needs a real random token; anything that pages by ID now gets time order for free.
  5. Optionally rebuild the index during a quiet window (REINDEX INDEX CONCURRENTLY orders_pkey; in PostgreSQL) to compact the half-empty pages the v4 rows left behind.

One more rule: do not use the v7 timestamp as your created_at. It is the moment the ID was generated, possibly on a client with a wrong clock, and PostgreSQL's uuidv7() even accepts an interval that shifts it. Keep a real timestamp column for business logic and treat the embedded one as a debugging aid.

FAQ

Is UUID v7 safe to put in a public URL?

It is safe in the sense that it reveals nothing but its creation time, to the millisecond. If that time is sensitive, such as account sign-up dates, use a v4 as the public ID and keep v7 internal.

Can two UUID v7 values collide?

One generator with a monotonic counter never repeats itself, because every ID is larger than the previous one. Two generators would have to pick the same counter and random bits in the same millisecond, which is far less likely than the hardware failing.

Can v4 and v7 live in the same column?

Yes. Both are valid RFC 9562 UUIDs in the same 16 bytes, so a uuid, BINARY(16) or uniqueidentifier column stores both. Switch the default for new rows and leave the old keys as they are.

Is UUID v7 slower to generate than v4?

The difference is a clock read and a counter increment, which is negligible next to a database round trip. The gain shows up in the index, not in the generator.

Open the UUID generator

Related reading