UUID v4 vs v7: Sortable IDs in New Projects
UUID v4 is random. UUID v7 is time-ordered, which keeps new rows closer together in a database index. When to pick v7 for a new API, and when v4 still fits.
By Rehan Baqir
Related
UUID v7 leaks its creation time, down to the millisecond, to anyone who can read the ID. UUID v4 does not, and its random bits scatter new primary-key inserts across the whole B-tree. On a small table that scatter is cheap. Once a table reaches the tens of millions of rows, Postgres or MySQL pulls cold index pages, splits them, and writes dirty pages back on almost every v4 insert. v7, specified in RFC 9562, keeps the 128-bit format and the same distributed generation, and it puts a 48-bit millisecond timestamp in the high bits so new rows append in rough time order. Generate a sample with the UUID generator. The bit layout is in what a UUID is. Check the version character in the regex tester, and inspect the JSON in the JSON formatter, once you are validating a string you already decided to generate.
UUID v4
- 122 random bits, plus 6 fixed version and variant bits (the version nibble is
4) - No embedded timestamp, so a URL does not reveal when the row was created
- Poor B-tree locality, because inserts land across the whole key space
For the bit walkthrough, see how UUIDs work and UUID in API development.
UUID v7
- A 48-bit Unix epoch timestamp in milliseconds, in the most significant bits
- 74 random bits, plus version
7and the variant bits, so nodes can generate IDs without coordinating - Monotonic inside one millisecond when the generator increments a sub-millisecond counter
- The creation time is visible. Anyone with the ID can read the millisecond it was minted
Side-by-side
| Aspect | UUID v4 | UUID v7 | | --- | --- | --- | | Bit layout | 122 random bits | 48-bit ms timestamp + 74 random bits | | Sortable by creation time | No | Yes (to the millisecond) | | B-tree insert locality | Poor on large tables | High: new keys append near the right edge | | Leaks creation timestamp | No | Yes (48-bit ms epoch in the prefix) | | RFC standard | RFC 4122 / RFC 9562 | RFC 9562 (2024) |
Worked example: the timestamp inside a UUID v7
Take this UUID v7:
018f3e2a-4c00-7a3b-9f12-8c4d2e1a9b05
- The third group is
7a3b. The leading7is version 7. - Strip the hyphens from the first 12 hex digits (
018f3e2a4c00). That 48-bit hex value is the Unix timestamp in milliseconds:1,714,734,844,928ms, which is May 3, 2024. - An ID generated a second later starts with a slightly larger hex prefix, so sorting the UUID column sorts roughly by creation time.
A v4 UUID from the UUID generator does the opposite. Consecutive calls jump the first 12 hex digits from 2f... to e9... to 04....
When v7 fits
- High-write OLTP tables (orders, events, audit logs, messages) that use a UUID primary key
- Services that generate IDs in the application, with no central database sequence
- Keyset pagination where sorting by
idshould roughly follow time
When v4 fits
- Unlisted share links, password-reset tokens, or invite IDs, where a visible creation time or sequence would help someone estimate activity
- Small or moderate tables, under a few million rows, where B-tree page splits are already rare
- Public resource IDs, if two IDs taken an hour apart would tell a competitor your daily order or signup volume
A hybrid that keeps both: store UUID v7 as the internal clustered primary key (id) and expose a separate UUID v4 (public_id), or another opaque token, in external URLs.
Database storage
PostgreSQL: store v4 and v7 in the native uuid column (16 bytes, not varchar(36)). Keep a separate created_at timestamptz column for reporting. Pulling the timestamp back out of the UUID is a weak substitute for that column.
MySQL / InnoDB: InnoDB clusters the table on the primary key, so a random v4 key fragments the row store. Secondary indexes on that key fragment with it. UUID v7 in BINARY(16) avoids the string width and that clustered-index churn.
Validation: in the regex tester, allow version 7 in the character class ([1-8], not a legacy [1-5] only). Inspect API payloads with the JSON formatter.
ULID and Snowflake
ULID packs a 48-bit millisecond timestamp and 80 random bits into a 26-character Crockford Base32 string (01ARZ3NDEKTSV4RRFFQ69G5FAV). It is shorter and safe in a URL. It does not fit a native SQL uuid column without a conversion step.
Snowflake and other 64-bit IDs fit in a BIGINT (8 bytes instead of 16), so the index is about half the size. Nodes need distinct worker IDs or they will collide.
Limitations
Anyone who can read a UUID v7 can decode the millisecond it was minted. A raw v7 value is a poor security token.
If one app server's NTP clock sits 200 ms behind another, its v7 IDs sort slightly behind IDs created at the same wall-clock moment on the other node. Keep created_at when the audit has to be strict.
Rewriting a 100-million-row production table from v4 to v7 locks the table or bloats storage while it runs. Leave a stable v4 table alone, and use v7 on new tables that are still growing fast.
Which version does which job
Use UUID v7 for new database primary keys when insert throughput and B-tree cache locality are the constraint. Keep UUID v4 where the ID must not show when it was created. Prototype the format in the UUID generator, and update regex validators so they accept version 7.
Frequently Asked Questions
Common questions answered to help you get the most from this tool.