Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

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

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 pr

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

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

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

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?

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.