Skip to content

UUIDv7 Is a Great Database Key and a Bad Clock: 4 Tests Before You Migrate

Published Last updated 6 min read

Key takeaways

  • UUIDv7 puts a millisecond timestamp in the high bits, so new IDs land near the end of a B-tree index instead of on random pages like UUIDv4.
  • RFC 9562 lets implementations alter the timestamp and makes no guarantee about how close it is to real time.
  • In the cited test, setting the system clock back one hour made Python's uuid7() timestamp run about an hour ahead of real time, and the gap kept growing.
  • UUIDv7 has at most 74 random bits, and in Python part of that is a counter, so never use it as a reset token, session ID, or unguessable link.
  • Rule of thumb: UUIDv7 for primary keys, a created_at column for time, and a random token for anything that grants access.

UUIDv7 solves a real database problem. It also quietly invites a bad assumption: that the ID tells you the time, and that time is a safe thing to show the world.

A post from October 19, 2026, “UUIDv7 doesn’t have the properties you think it does”, takes that assumption apart with a test you can reproduce. I still recommend UUIDv7. I just want you to know what you are signing.

TL;DR (for non-technical readers)

  • A new kind of ID number, UUIDv7, is popular because it keeps databases fast and still avoids collisions.
  • Each ID has the creation time baked in. That is handy, but it also means anyone who sees an ID can tell roughly when something was created, and the time is not always accurate.
  • It is a good internal label and a poor secret. Never use it as a password-like link.
  • Ask your tech team: “Do any of our public links, or any sorting logic, depend on these IDs being in time order?”

Why UUIDv7 is good

Random UUIDv4 values land all over a B-tree index. Inserts hit random pages, pages split, caches go cold. UUIDv7 puts a millisecond timestamp in the high bits, so new IDs land near the end of the index like an auto-increment would, while staying unique without a central coordinator.

Bit layout of a UUIDv7: 48-bit millisecond timestamp, version and variant bits, and up to 74 random bits of which 42 are a counter in Python.

Python 3.14 added uuid.uuid7() to the standard library. Per the docs, it embeds a 48-bit timestamp and uses a 42-bit counter to keep IDs monotonic within a millisecond.

What the article found

From the post and RFC 9562:

Diagram of the clock-jump test: after the system clock is set back one hour, the uuid7 timestamp runs about an hour ahead of real time and the gap keeps growing.

  • The RFC lets implementations alter the timestamp, and says it makes no guarantee about how close it is to real time.
  • Implementations may also move the timestamp ahead of the real clock to preserve ordering.
  • In the author’s test with Python’s uuid7(), the system clock was set back one hour. The embedded timestamp then ran ahead of real time by roughly an hour, and the gap kept growing as IDs were generated at volume.
  • The only guarantee the author trusts across implementations is collision avoidance and uniqueness.

PostgreSQL’s docs say something similar: the extracted timestamp is not necessarily equal to when the UUID was generated, and that depends on the implementation.

What practitioners are saying

An older but still relevant Hacker News thread on UUIDv7 as a primary key shows the split well.

  • “Leaking time is fine.” One commenter argued that with proper authorization checks, predictable IDs are harmless, and a creation time rarely matters to most companies.
  • “74 random bits is plenty.” Another noted that the random portion is far larger than most secret keys people use, so guessing an ID is impractical.
  • “Mixed ID types leak more.” A third pointed out that mixing v4 and v7 can tell outsiders which resources the team considered sensitive, and that enforcing consistency across a large team is real work.
  • “Pagination is an oracle.” Someone else noted that cursors like id > some_value can be abused even without decoding anything.
  • “Encrypt the public one.” A database administrator suggested keeping v7 internally and exposing an encrypted or separate identifier outward.

My take

The pragmatists are right about guessing and the cautious voices are right about metadata. Those are different questions, and the debate gets muddy when people argue them together.

I would use UUIDv7 as an internal primary key without much hesitation. It is a real improvement for write-heavy tables, and the cost is small if you treat the timestamp as a hint.

Where I disagree with “leaking time is fine”: that is a policy decision, not a technical default. For a support ticket, fine. For an account, an order, a medical record or a whistleblower report, creation time can reveal things you never decided to publish. Make that call on purpose.

I also think the “clock” complaint is slightly overstated in practice, and still worth testing. A clock that jumps back an hour is rare on a healthy server, but VMs get paused, NTP gets misconfigured, and containers get migrated. If your code sorts by ID to mean “newest”, you are depending on something the spec never promised. A created_at column costs almost nothing and removes the doubt.

For teams: choose UUIDv7 for keys, a timestamp column for time, and random tokens for access. Three jobs, three tools.

Try it in 15 minutes

1. Tight-loop ordering

import uuid

ids = [uuid.uuid7() for _ in range(200_000)]
out_of_order = sum(1 for a, b in zip(ids, ids[1:]) if b < a)
print("out of order:", out_of_order)

Expect zero in one process on Python 3.14. Then run the equivalent in the library you actually deploy. Not every implementation guarantees it.

2. Multiple processes

# gen.py
import uuid

for _ in range(100_000):
    print(uuid.uuid7())
for i in 1 2 3 4; do python gen.py > out_$i.txt & done; wait
cat out_*.txt | sort | uniq -d | wc -l
for f in out_*.txt; do sort -c "$f" && echo "$f sorted"; done

Duplicates should be zero, and each file should be ordered. The merged stream across processes is only roughly time-ordered, because each process has its own counter.

3. Decode what your IDs reveal

import uuid, datetime

u = uuid.uuid7()
print(u)
print(datetime.datetime.fromtimestamp(u.time / 1000, tz=datetime.timezone.utc))

Now take an ID from your own system. Is it in a URL, an email link or a public API? Anyone holding it learns when the object was created.

4. Never use it as a secret

UUIDv7 has at most 74 random bits, and in Python part of that is a counter. Do not use it as a reset token, session ID or “unguessable link”:

import secrets
print(secrets.token_urlsafe(32))

Rule of thumb

JobUse
Primary key in your databaseUUIDv7
”When did this happen”A created_at column
Anything that grants accessA random token
Public identifierDecide deliberately whether creation time may leak

Rule of thumb table: UUIDv7 for primary keys, created_at for time, random token for access, deliberate decision for public identifiers.

What to learn next

Question

Is UUIDv7 an opaque key in your system, or does some query, cursor or report quietly assume it is in time order?

Originally published on DEV. Discussion happens there.

Frequently asked questions

Is UUIDv7 a good primary key for a database?
Yes for write-heavy tables, because the timestamp in the high bits makes inserts land near the end of the index while IDs stay unique without a central coordinator. The author would use it as an internal primary key without much hesitation.
Does a UUIDv7 reveal when a record was created?
Yes. Anyone holding the ID can decode the embedded timestamp, which may not be exact. For accounts, orders, or medical records, decide on purpose whether creation time may leak.
Can I use UUIDv7 as a secret token or reset link?
No. It has at most 74 random bits, and in Python part of that is a counter. Use secrets.token_urlsafe(32) instead.
Can I sort by UUIDv7 to mean newest?
Not safely. The spec never promised the timestamp matches real time, and in the cited test a clock set back one hour pushed the embedded timestamp about an hour ahead of real time. A created_at column removes the doubt.
Share Share on X Share on LinkedIn

About the author

Security Engineer & Technical Writer

Security engineer and technical writer based in Batam, Indonesia. I review and harden MCP servers and AI agents, build security automation, and write up what holds and what fails. I also train on DNS security as a PANDI .id Academy trainer.

All writing | RSS | Markdown