Pick a UUIDv7 library that keeps order: 10 tested, .NET fails 50%

On this page · 16 sections
- Key numbers
- Why this came up tonight
- What I tested, and how
- Finding 1: .NET's UUIDv7 is a coin flip inside a millisecond
- Finding 2: uuid6 on PyPI runs fast with nothing wrong at all
- Finding 3: the PyPI package named "uuid7" dates your rows to 2202
- Finding 4: Python's standard library never comes back from a clock jump
- Finding 5: "freeze" is the common answer, and it is OK
- Finding 6: the only two that passed were written by one person
- The scorecard
- Check your own stack in 10 seconds
- Honest caveats
- The numbers so far
- My bet, and a question
- If your IDs sit in your URLs
- Sources
Short answer: Not every UUIDv7 library gives you sorted IDs with a true timestamp. We ran 10 popular ones (Rust, Python, JavaScript, Go and .NET) through 1 million IDs each plus a simulated clock that steps back an hour. Microsoft's Guid.CreateVersion7() put 50.0% of IDs out of order in a tight loop, the uuid6 package on PyPI stamped IDs 16.6 minutes in the future with a healthy clock, and the PyPI package literally named uuid7 writes IDs that RFC 9562 readers date to the year 2202. The two libraries that passed everything were written by the same person.
TL;DR: If you are on Rust or Node and want both order and an honest timestamp, use LiosK's uuid7 crate or uuidv7 on npm. The Rust uuid crate and npm uuid are fine unless your clock steps back. On Python, the standard library's uuid.uuid7() is safe until the clock jumps backwards, then it never catches up. On .NET, never sort on CreateVersion7() inside a millisecond. Checked Oct 23, 2026, 9:20 PM ET. Every number below is in the CSV.
Key numbers
| What | Number | Source, checked Oct 23 2026 |
|---|---|---|
| Libraries tested | 10, across 5 languages | versions in the table below |
| IDs per library, tight loop | 1,000,000 | our harness, Intel i7-13700KF, Windows 11 |
.NET CreateVersion7() out of order | 50.0% (tight loop), 24.9% at just 2,000 IDs/s | our test, .NET 10.0.13 |
| Every other library, out of order in a tight loop | 0 | our test |
uuid6 (PyPI) timestamp after 1M IDs, healthy clock | 16.6 minutes ahead | our test, uuid6 2025.0.1 |
uuid6 downloads last month | 10,858,926 | pypistats |
PyPI uuid7 IDs, decoded per RFC 9562 | year 2202 | our test, uuid7 0.1.0 (11,048,710 downloads last month) |
| Python stdlib after the clock steps back 1 h, at 10,000 IDs/s | 60 min ahead, then 78 min 2 minutes later, still climbing | our test, CPython 3.15.1 |
| Libraries that kept order and a true clock in both tests | 2, both by LiosK | our test |
Download the data (CSV, 1,489 rows: the tight-loop result for all 10 libraries, 27 clock-jump runs and 3 healthy-clock uuid6 runs, sampled every 2.5 simulated seconds. Every row names the library, version and test.)

Why this came up tonight
Robert Alexander's post "UUIDv7 doesn't have the properties you think it does" hit the Hacker News front page today (109 points, 94 comments when I checked). His argument: the three things every UUIDv7 blog post promises, sorted IDs, a useful creation timestamp and unguessable IDs, are not strictly guaranteed by RFC 9562. He showed Python's standard library racing ahead of the clock after a time change.
He is right, and the spec says so out loud. Section 6.1 reads: "Implementations MAY alter the actual timestamp." So the useful question is not "is UUIDv7 good". It is: which library you actually import does what. Nobody had lined them up. So I did.
What I tested, and how
Ten libraries, picked by reach: the default in each language plus the packages people actually install.
| Language | Library | Version | Reach |
|---|---|---|---|
| Rust | uuid Uuid::now_v7() | 1.29.0 | 226.2M downloads in 90 days |
| Rust | uuid7 (LiosK) | 1.8.0 | 46,634 in 90 days |
| Python | standard library uuid.uuid7() | CPython 3.15.1 | ships with Python 3.14+ |
| Python | uuid6 | 2025.0.1 | 10.9M last month |
| Python | uuid7 (imports as uuid_extensions) | 0.1.0 | 11.0M last month |
| Python | uuid-utils (Rust core) | 1.1.0 | 69.8M last month |
| JavaScript | uuid v7() | 14.0.3 | 173.7M last week |
| JavaScript | uuidv7 (LiosK) | 1.2.1 | 2.0M last week |
| Go | google/uuid NewV7() | 1.6.0 | imported by 113,457 packages |
| C# / .NET | System.Guid.CreateVersion7() | .NET 10.0.13 | built into .NET 9+ |
Download counts are from crates.io, pypistats, the npm downloads API and pkg.go.dev, all read tonight. (Raw download counts are inflated by CI and bots; our crates.io study found most crate downloads land on release days. They still rank reach fine.)
Test 1, the tight loop. One thread makes 1,000,000 IDs as fast as the library allows, reading the wall clock every 10,000 IDs. I count how many IDs sort at or below the one made just before them (byte order, the way a database index sees them), duplicates, and how far the embedded timestamp drifts from the real clock.
Test 2, the clock steps back an hour. I can't yank the clock on a real machine without wrecking it, so I injected a fake clock into each real library: Python's time.time_ns, JavaScript's Date.now, Go's internal timeNow hook (one added line in a vendored copy), the explicit-timestamp APIs in Rust and .NET. The fake clock runs normally for 1 second, then steps back 3,600,000 ms, then runs 120 more seconds. I ran it at 1, 2 and 10 IDs per millisecond. A one-hour step is not exotic: a Windows feature reset clocks by months in 2023, and Robert collects more in his clock skew post.
The harness code for all five languages is short and boring on purpose. If you want the 10-second version of the order check for your own stack, it is at the end.
Finding 1: .NET's UUIDv7 is a coin flip inside a millisecond
Microsoft shipped Guid.CreateVersion7() in .NET 9. It is the obvious choice for every C# developer who read "UUIDv7 is sortable" and moved on. In our tight loop, 500,470 of 999,999 IDs (50.0%) sorted before the ID made right before them.

The reason is in Guid.cs: it takes a random NewGuid(), writes the millisecond timestamp over the top 48 bits, and stops. No counter. Inside one millisecond the other 74 bits are pure random, so any two IDs from the same millisecond are a coin flip. The docs say only that it "seeds the rand_a and rand_b sub-fields with random data". RFC 9562 allows that (counters are optional), so this is legal. It just isn't what people think they are getting.
You don't need a benchmark loop to hit it. At a steady 2,000 IDs per second, 24.9% of IDs landed out of order in our run. At 10,000 per second, 45.0%. At 1,000 per second, almost none, because each ID gets its own millisecond.
A second .NET trap, while we are here: Guid.ToByteArray() writes the first 8 bytes little-endian. Store those bytes in a binary(16) column and the lowest byte of the timestamp sorts first, so the order breaks every 256 ms. I counted 234 backward steps per minute at one ID per millisecond. Use ToByteArray(bigEndian: true) (or the string form) if order matters.
Fair point for Microsoft: the timestamp itself is always exactly the clock. When our fake clock stepped back an hour, .NET's IDs went back an hour too. Honest time, no order. It is the opposite trade from the next one.
Finding 2: uuid6 on PyPI runs fast with nothing wrong at all
This one surprised me most. uuid6's uuid7() keeps the last timestamp it used. If the clock hasn't moved past it, it uses the last timestamp plus 1 ms. Every time.
# uuid6 2025.0.1, src/uuid6/__init__.py
timestamp_ms = nanoseconds // 10**6
if _last_v7_timestamp is not None and timestamp_ms <= _last_v7_timestamp:
timestamp_ms = _last_v7_timestamp + 1
That buys strict order with no counter bits. The bill: whenever your app makes more than one ID per millisecond, the timestamp walks into the future one millisecond per ID. Our 1,000,000 IDs took about 1.3 seconds and ended 998,688 ms (16.6 minutes) ahead of the real clock. Every single ID got its own millisecond.

Real traffic does it too, just slower. With a healthy fake clock at 2,000 IDs per second, uuid6 was 60 seconds ahead after one minute. At 10,000 per second it was 9 minutes ahead after one minute. Real traffic is bursty, so even an app averaging under 1,000 per second will drift during its spikes and only creep back in quiet periods. 10,858,926 downloads last month. If you sort or partition by the embedded time, or call uuid_extract_timestamp() on these, you are reading a number the library made up.
Finding 3: the PyPI package named "uuid7" dates your rows to 2202
Search PyPI for uuid7 and you get uuid7 0.1.0 by Stephen Simmons, uploaded Dec 29, 2021, imported as uuid_extensions. It pulled 11,048,710 downloads last month. It sets the version nibble to 7. But it was written against an early draft, before RFC 9562 fixed the layout: the top bits hold time in units of 16 seconds plus a 24-bit fraction, not Unix milliseconds.

So any tool that reads the first 48 bits as RFC 9562 says, like Postgres' uuid_extract_timestamp(), gets September 14, 2202. And mixed into a table with real UUIDv7 IDs, every one of these sorts after every standard one. Its ordering inside its own world was fine in our tight loop (0 out of order), and when the clock stepped back it simply followed it back (1 out-of-order ID). The package is not malicious, it is just old. But 11 million downloads a month of a pre-standard format with the standard's version number on it is a trap with a nice name.
"Implementations MAY alter the actual timestamp." RFC 9562, section 6.1
Finding 4: Python's standard library never comes back from a clock jump
CPython's own uuid.uuid7() (new in 3.14) passed the tight loop: 0 out of order, timestamp within a millisecond of the clock. It uses a 42-bit counter, as the RFC suggests. The problem is the backward-clock branch, which Robert flagged and CPython issue #138862 discusses:
# CPython 3.15.1, Lib/uuid.py
if timestamp_ms < _last_timestamp_v7:
timestamp_ms = _last_timestamp_v7 + 1
Read it slowly. If the clock is behind the last ID, the new ID gets the last timestamp plus 1 ms, and that becomes the new "last". So while the clock is behind, every ID pushes the stored time 1 ms further ahead. Make more than 1,000 IDs a second and the gap grows forever. In our run at 10,000 IDs per second, the gap was 60 minutes right after the jump and 78 minutes two minutes later, growing 9 minutes per minute. At 2,000 per second it grew 1 minute per minute. At exactly 1,000 per second it never shrinks. The same line is in 3.14.9 and main today.
The only exit is a process restart, which wipes _last_timestamp_v7. Then new IDs jump back to the true time and sort before everything made during the runaway. That is the worst of both worlds: wrong times, then broken order.
The CPython devs' view in that thread is that this is consistent with the RFC and other implementations, and the RFC does allow incrementing ahead. My take: allowed is not the same as fine. Rust's uuid crate, npm uuid and Go all freeze instead of racing, and that is a much kinder failure.
Finding 5: "freeze" is the common answer, and it is OK
The Rust uuid crate (timestamp.rs), npm uuid (v7.ts) and Go's google/uuid (version7.go) all do the same thing when the clock steps back: keep the last timestamp and bump a counter. The gap starts at 60 minutes and shrinks one second per second, so the IDs carry a time up to an hour off, then everything is normal again. Order never breaks. uuid-utils on PyPI wraps the Rust crate's now_v7(), so it inherits this (I didn't simulate it separately: its clock is inside the compiled module).
Go has one wrinkle. It stores a 12-bit fraction of the millisecond instead of a counter, and bumps that by one step when two IDs collide. That caps it at 4,096 IDs per millisecond before it starts borrowing from the future. Our tight loop ran at 9 million per second, so it ended 135 ms ahead. You will not hit that in a web app. A batch job might.
Finding 6: the only two that passed were written by one person
LiosK's uuid7 crate and uuidv7 npm package use the same design: a 42-bit counter for order, and a rollback_allowance of 10 seconds. A small step back (NTP jitter) is absorbed by the counter. A step back of more than 10 seconds resets the generator to the real clock. In our runs that cost exactly 1 out-of-order ID at the moment of the jump, and every timestamp after it was true.
That is the honest trade the spec can't make for you: one broken sort at a once-a-year event, in exchange for never lying about time. Both were also the fastest in their language (24 million IDs per second for the crate).

The bit layout explains the rest of the trade. Counter bits buy order inside a millisecond, but they are not random: the next ID from the same millisecond is the last one plus one. Python's stdlib and the LiosK libraries keep 32 fresh random bits per ID, npm uuid 42, Go 62, .NET and uuid6 the full 74. If IDs in your URLs are the only thing stopping a stranger reading someone else's record, Robert has a whole post on that: UUIDs and IDOR. 32 random bits is still about 4 billion guesses per neighbor, which is a lot, but it is not 74.
The scorecard

And the picks, by language:
- Rust:
uuid7if a true timestamp matters to you,uuid'snow_v7()if you just want sorted IDs. Both are fine. (If you write databases in Rust, ordering bugs like this are exactly what bit us in our AI-built SQL engine.) - Node:
uuidv7overuuidif you care about the time; otherwiseuuidis fine. - Python:
uuid-utils(Rust core, freezes instead of racing). The stdlib is OK if your clocks are disciplined and you restart rarely. Dropuuid6'suuid7()and theuuid7package. - Go:
google/uuidis fine below 4 million IDs per second. - .NET:
CreateVersion7()only if you never rely on order inside a millisecond, and never sort onToByteArray()bytes. Otherwise use a counter-based library.
Check your own stack in 10 seconds
Paste this into whatever language you use, swap in your generator, and count. Anything above 0 means your IDs are not sorted the way you think.
# Python 3.14+: count IDs that sort at or below the previous one
import uuid
ids = [uuid.uuid7().bytes for _ in range(1_000_000)]
print(sum(b <= a for a, b in zip(ids, ids[1:])))
// C#: same check for Guid.CreateVersion7()
var ids = Enumerable.Range(0, 1_000_000).Select(_ => Guid.CreateVersion7().ToString("N")).ToArray();
Console.WriteLine(ids.Zip(ids.Skip(1)).Count(p => string.CompareOrdinal(p.Second, p.First) <= 0));
To check the timestamp, compare id.int >> 80 (Python) or the first 12 hex digits against your clock in milliseconds right after the loop. More than a few ms apart means your library is inventing time.
Honest caveats
- One machine, one thread, Windows 11 on an i7-13700KF. Multi-process generators were not tested; none of these libraries coordinate across processes anyway.
- The clock jump is simulated by injecting a fake clock into the real library code, not by changing the system clock. For Go that meant one added setter in a vendored copy of the module; the generator code is untouched.
uuid-utilswas run on Python 3.12 (its wheel is built for 3.12), where the wall clock is read at 15.6 ms resolution. Its 10 ms max reading is inside that and I count it as 0.- The RFC permits all of these behaviors. "Fails" here means "does not give you what UUIDv7 posts promise", not "violates the spec". The PyPI
uuid7package is the exception: its layout predates the RFC.
The numbers so far
| Measure | Result |
|---|---|
| Libraries with 0 out-of-order IDs in 1M | 9 of 10 |
| Libraries with a true timestamp under load | 7 of 10 (uuid6, PyPI uuid7 and Go did not) |
| Libraries that recover after a 1 h backward step | 8 of 10 (Python stdlib and uuid6 run away) |
| Libraries that passed every test | 2 (uuid7 crate, uuidv7 npm) |
| Combined monthly PyPI downloads of the two Python packages to drop | 21.9 million |
My bet, and a question
My bet: CPython changes that backward-clock branch to freeze instead of increment within a year, because "the timestamp is an hour off and growing" is going to show up in someone's incident report. And .NET ships a counter option, because "sortable GUID" is the whole reason people reach for v7.
Which one is your stack on? If you have a library I didn't test (Java's uuid-creator, Postgres 18's uuidv7(), Ruby, PHP), run the 10-second check and tell me the number in the comments. I'll add it to the dataset.
If your IDs sit in your URLs
An ID in a URL is fine as long as guessing one gets a stranger nothing. The real question is whether someone who changes the ID can read a row that isn't theirs. If you built your app with Lovable, Bolt or Supabase, that is the first thing our $49 Launch Check tests: can a stranger read your tables, plus exposed keys, storage buckets and rate limits, in plain English within 48 hours. Guaranteed: if a problem in what we checked turns up after your report, we review it and write the fix at no extra cost. More free tools are on the tools page, and the rest of our Rust work is under Rust.
As featured in This Week in Rust.
Sources
- Robert Alexander, UUIDv7 doesn't have the properties you think it does (Oct 19, 2026), and UUIDs and IDOR; GitHub. Hacker News thread: item 50211746.
- RFC 9562, sections 6.1 and 6.2.
- CPython Lib/uuid.py (v3.15.1) and issue #138862.
- uuid6-python by oittaa; uuid7 by Stephen Simmons; uuid-utils by Amin Alaee.
- uuid-rs/uuid; uuid7-rs and uuidv7 by LiosK; uuidjs/uuid; google/uuid.
- .NET Guid.cs and CreateVersion7 docs.
- PostgreSQL UUID functions.
- Download counts: pypistats.org, npm downloads API, crates.io, pkg.go.dev, read Oct 23 2026.
- Ars Technica, Windows feature that resets system clocks (2023).
- Our data: data.csv.
First published here on tensorust.com on Oct 23, 2026. Related: crates.io download checker, Build a database in Rust, Launch Check.
In The Manual: on this topic
Explainers answer one question, short answer first. Guides walk you through it step by step.
- Explainer · Shipping and sellingUUIDv7 vs UUIDv4: which should you use?
- Explainer · Rust and cratesWhy are crates.io download counts inflated?
- Explainer · AI, LLMs and agentsWhat is x402?
- Guide · Shipping and sellingHow to deploy to Vercel from the CLI
Get the next one by email.
What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.
No spam, never sold. Every email has a one-click unsubscribe.
Read next
Free tool: iPhone app cost calculator → Pick an app type and features, get an honest budget range.
- Dev · Oct 19, 2026 · 11 min readSelling on Gumroad with AI: 130 products, 0 sales. Greg is wrong
- Dev · Oct 19, 2026 · 15 min readApple's 4.3 spam check is a 30-second batch job. 22 rejections prove it
- Gaming · Oct 19, 2026 · 18 min readBalatro save file runs as code. 1 in 5 corrupted saves load anyway