Skip to content
UUID v4 vs v7 — When Timestamp-Ordered IDs Actually Matter

Fundamentals

UUID v4 vs v7 — When Timestamp-Ordered IDs Actually Matter

UUID v7 adds a millisecond timestamp prefix to random bytes. Here is when that matters for Postgres, MySQL, and distributed systems, and when v4 is still fine.

UUID v7 landed in RFC 9562 (May 2024) and most language standard libraries now ship it. It is the first real alternative to v4 for primary keys since v4 became the default sometime in the early 2010s. This guide is the when-to-use-which.

Quick mental model

A UUID v4 is 122 bits of randomness wrapped in a 36-character string. A UUID v7 is a 48-bit millisecond timestamp followed by 74 bits of randomness, also 36 characters. Both are globally unique with astronomically high probability.

The single difference that matters for systems design: a sorted list of v7s is in time order. A sorted list of v4s is in no order at all.

v4:  f47ac10b-58cc-4372-a567-0e02b2c3d479   (all random)
v7:  018f4bea-7e83-7abc-9d0e-0a1b2c3d4e5f
     └─timestamp─┘v└─rand──┘v└──random──┘

Both use the same 8-4-4-4-12 hex format and both fit in a 16-byte binary column.

Where v4 is fine

If your primary key is stored in a hash-partitioned table, an in-memory key-value store, or a NoSQL document store that indexes by hash anyway, you do not need v7. Random distribution is a feature there.

Use v4 when:

  • Your database is MongoDB, DynamoDB, Cassandra, or any hash-partitioned store.
  • You never need to range-scan, range-page, or bulk-insert into a B-tree index by ID.
  • You deliberately want unpredictable IDs in URLs for enumeration resistance. This is only weakly true — both v4 and v7 have enough random entropy to prevent brute-force enumeration. The difference is that v7 leaks creation time; if that is a leak you care about, v4 is safer.

Most language runtimes now have a native generator. crypto.randomUUID() has been in Node since 14.17 (2021) and in all evergreen browsers since early 2022. Python’s uuid.uuid4() is standard library. Postgres has gen_random_uuid() built in since v13.

Where v7 is the better default

The B-tree index problem is the one to internalize.

Postgres, MySQL/InnoDB, SQLite, and almost every OLTP database stores rows clustered or indexed by primary key in a B-tree. Inserting into a B-tree is cheap when the new key is greater than the last key (the right edge of the tree) and expensive when it is random (every insert dirties a different leaf page, cache locality collapses, page splits cascade).

In practice, v4 primary keys cost 2-5× the insert throughput of auto-incrementing integers or v7 UUIDs on a loaded table. The effect is not visible on a laptop benchmark with a few thousand rows — it shows up under write pressure with an index that no longer fits in RAM.

Workloadv4v7auto-increment int
Insert into indexed PKSlow (random)Fast (monotonic)Fastest
Range scan by creation timeNeeds created_at indexWorks on PKWorks on PK
Clustering on diskRandomTime-orderedTime-ordered
Leaks creation timeNoYesYes (sort of)
Globally unique across nodesYesYesNo (coordination needed)

Use v7 when:

  • The ID is a primary key on a B-tree database and the table will grow past a few million rows.
  • You want to sort by creation order without a separate created_at column, or you want rough time-range queries on the PK.
  • You are building distributed event logs where rough time ordering at the ingestion tier helps consumers.

Generating v7 in code

The RFC is straightforward: first 48 bits are the Unix timestamp in milliseconds, a 4-bit version (0x7), 12 bits of randomness for intra-millisecond ordering, a 2-bit variant, and 62 bits of random tail.

# Python 3.13+: uuid.uuid7() is standard library
import uuid
uuid.uuid7()
# UUID('018f4bea-7e83-7abc-9d0e-0a1b2c3d4e5f')

# Older Python or any version where you want explicit control:
import os, time
def uuid7() -> str:
    ts = int(time.time() * 1000).to_bytes(6, "big")
    rand = os.urandom(10)
    b = bytearray(ts + rand)
    b[6] = (b[6] & 0x0F) | 0x70   # version 7
    b[8] = (b[8] & 0x3F) | 0x80   # variant
    h = b.hex()
    return f"{h[:8]}-{h[8:12]}-{h[12:16]}-{h[16:20]}-{h[20:]}"

In Postgres you can install the pg_uuidv7 extension or generate it application-side. Node does not yet have a built-in crypto.randomUUID({ version: 7 }) — use the uuid npm package (v10+) which ships v7().

Migration and interop notes

Three practical points if you are moving an existing system:

  1. Do not mix v4 and v7 on the same PK column mid-table. You get the worst of both — the historic random tail keeps degrading the left side of the B-tree, and the new time-ordered tail only helps new pages. Either flip cleanly or accept that the benefit only applies to new tables.
  2. Time-leakage is real. A v7 reveals the millisecond of creation to anyone with the ID. For invoice numbers, order IDs, or user IDs, that is usually fine. For anything security-sensitive (password reset tokens, share links), use a separate random token — never a raw UUID of any version.
  3. Clock skew matters less than people fear. A clock that jumps backward by 10 seconds produces 10 seconds of out-of-order v7s and then recovers. The uniqueness guarantee still holds because of the random tail.

When to use neither

ULIDs (Crockford base-32, 26 chars) predate v7 and solve the same problem. If you already have ULID infrastructure, keep it; the on-disk win is the same. For internal monotonic IDs within a single database, a bigint with a sequence is still the cheapest primary key by a wide margin — use it unless you specifically need global uniqueness.

For token formats that build on Base64 rather than hex, the Base64 in production guide covers why a v4 UUID re-encoded as base64url is shorter but usually not worth the cognitive cost.

Takeaways

Default to v7 for database primary keys on new projects. Keep v4 when the DB does not care about insert order (hash-partitioned stores) or when you must hide creation time. Both are 128 bits of “basically unique”; the difference is whether your B-tree suffers. For auth token patterns and why UUIDs alone are not secrets, see the JWT in 2026 piece.

By ·