RFC 4122/9562: UUID version 7 and 8 implementation

We are nearing Python 3.14 beta (May 2025) and we need to decide on the implementation. UUID version 8 is now part of the 3.14 but UUID version 7 is still under discussion (me and myself are discussing with my other self).

TL;DR: Should we use sub-millisecond precision for UUID version 7 as it is done in PostgreSQL?

There is a lot of users that want UUID version 7 (this can be seen by the number of reactions on Support UUIDv6, UUIDv7, and UUIDv8 from RFC 9562 · Issue #89083 · python/cpython · GitHub and gh-89083: add support for UUID version 7 (RFC 9562) by picnixz · Pull Request #121119 · python/cpython · GitHub). UUIDv7 use cases are mainly for microservices, especially for database storage.

In December 2024, PostgreSQL exposed UUIDv7 (git.postgresql.org Git - postgresql.git/commitdiff). They however chose Method 3 instead of Method 1 as I did and as Rust currently does:

In our implementation, the 12-bit sub-millisecond timestamp fraction
is stored immediately after the timestamp, in the space referred to as
“rand_a” in the RFC. This ensures additional monotonicity within a
millisecond. The rand_a bits also function as a counter. We select a
sub-millisecond timestamp so that it monotonically increases for
generated UUIDs within the same backend, even when the system clock
goes backward or when generating UUIDs at very high
frequency. Therefore, the monotonicity of generated UUIDs is ensured
within the same backend.

The difference between a fixed-length counter and a submillisecond-based counter is the following:

  • Method 1: Fixed length counter have 42 bits but only a 48-bit timestamp (ms precision). While it’s likely to generate multiple UUIDs within the same millisecond, it’s very unlikely to overflow the 42 bits counter within that ms. And if we do, the current PR simply advances the timestamp (so the UUID would say that it was generated 1 ms after, while it was not necessarily the case).
  • Method 3: With submillisecond precision, it’s possible to extract a more precise timestamp for the UUID. I’m not well-versed enough in microservices to know whether submillisecond precision is really needed or not. I think what matters more is monotonicity, namely that UUIDs are monotonously generated. Now, with subms precision, it also allows future improvements to be backward compatible, such as offsetting the timestamp by a constant value (with method 1, we can only offset with ms precision, not subms prec). The difference is that we don’t have a counter anymore and we generate 62 random tail bits instead of 32 random tail bits.

Note that we only need UUIDs within the same millisecond before we need to advance to the next millisecond instead of billions of UUIDs for Method 1. However, it makes them a bit more unpredictable (for instance, if one knows that two UUIDs were consecutively generated within the same millisecond using Method 1 and if one knows the first UUID, we can recover 96 bits (because we would know the 42-bit counter and only the 32 tail bits would be random)). On the other hand, even if one knows that two UUIDs were generated consecutively within the same millisecond with Method 3 and even if we know the first UUID, the second UUID would still have 62 tail bits to guess.

Ideally, I don’t want to allow parameters to be passed to uuid7(). While it would make sense to offer other constructor functions, we first need to have kind of a “canonical” implementation (even with parameters, we need default parameters because users likely don’t want to call uuid7() with required parameters, and having multiple functions make the choice harder for any consumer).

Because of the above points, I’m considering switching to a sub-ms precision (Method 3). While it wouldn’t align with Rust, it would align with PostgreSQL, and I think UUIDs are likely to be used in microservices and thus it’s good to follow the same rationale.

UUID version 7 - Generation Method
  • Millisecond precision, 42-bit counter, 32 random tail bits, aligned with Rust.
  • Sub-millisecond precision, no counter, 62 random tail bits, aligned with Postgre.
  • Other (comment)
0 voters