UUIDv7 Is a Great Database Key and a Bad Clock: 4 Tests Before You Migrate
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.

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:

- 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_valuecan 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
| Job | Use |
|---|---|
| Primary key in your database | UUIDv7 |
| ”When did this happen” | A created_at column |
| Anything that grants access | A random token |
| Public identifier | Decide deliberately whether creation time may leak |

What to learn next
- RFC 9562, for the exact wording on timestamps and counters.
- The original UUIDv7 post, to see the clock-jump test in full.
- The PostgreSQL UUID functions docs, then run
EXPLAINon your own queries instead of trusting someone else’s benchmark.
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.
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.
Related articles
-
8 min read
We Planted 10 Vulnerabilities to Test Free Semgrep. It Reported 3.
We planted 10 vulnerabilities in a React and TypeScript app and ran free zero-config Semgrep. It reported 3 of 10. Here is why and how to close the gap.
- Security Engineering
- Engineering Practice
-
6 min read
PHP2Shell: The Patch Is Out. Is It Running in Your Containers?
PHP2Shell patches for CVE-2026-109618 and CVE-2026-109619 are out. Five checks to prove the fix is running in your containers, not just released.
- Security Engineering
- Deployment
-
6 min read
The eMudhra CA Keys Were One Byte Long. Does Your Container Still Trust Them?
Eight eMudhra CA keys were one-byte numbers. Browsers blocked them in hours, but containers may not. A 15-minute trust store audit for Docker, Python, Java.
- Security Engineering
- Deployment
All writing | RSS | Markdown