RFC 4122/9562: UUID version 7 and 8 implementation

TL;DR:

  • What would be the best implementation for UUID v7? should it solely focus on providing monotonicity within a milli-second and unguessability for the remaining components or should it provide an improved timestamp precision?
  • What would be the best implementation for UUID v8? should it solely focus on being the most generic one where the random blocks can be customized and no monotonicity is guaranteed, should it feature time-based properties or name-based properties?

RFC 9562, superseeding RFC 4122, is now in the standard tracks, meaning that it is part of the standard. There are many implementations of RFC 9562, as acknowledged by the following study: GitHub - uuid6/prototypes: Draft Prototypes and Tests for UUIDv6 and beyond.

While the implementation of version 6 is explicitly stated in the RFC, versions 7 and 8 may have different purposes and different implementations. There has been a previous discussion on UUID v7 (Add uuid7 in uuid module in standard library) but the latter did not discuss the implementation details of the UUIDv7 nor UUIDv8. A first PR was drafted for v6/v7 in 2021 by oittaa #29824 but was based on previous specifications. The same author updated their implementation oittaa/uuid6-python but did not make another PR on that topic.

I want to discuss the implementation of UUIDv7/v8 for the standard library. Since Python is not the only language in which UUIDs are implemented, I would appreciate the feedback of any other language implementing them at the level of their standard library.

Version 7

For version 7, the UUID consists of a 48-bit timestamp (milliseconds precision) from UNIX epoch together with 74 bits of randomness split into two (non-adjacent) blocks of 12 and 62 bits. The content of those blocks depends on the implementation. The RFC allows the following constructions:

  • Select 74 bits uniformly at random. This is the simplest way to implement version 7 while maintaining monotonicity within a millisecond. This is implemented in #120650.
  • Use 12 bits for sub-millisecond precision and the remaining 62 bits for a seeded counter. There are various methods for implementing this approach, one given by #120830 and another one given by stevesimmons/uuid7. There is also a rust implementation of v7 uuid-rs/uuid but this one is again a bit different (at least in the way it samples the components).

There are pros and cons for those two approaches. The first approach gives you a relatively good entropy but not much of a timestamp precision. However, the purpose of v7 is to improve the entropy characteristics over UUIDv1 or UUIDv6. In particular, you may want to balance the precision and the randomness constraints in such a way that you can batch-generate UUIDv7 objects without having too many collisions, while being better than UUIDv1 or UUIDv6 (the RFC states that “Implementations SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6 if possible”, so it can be thought of as a replacement).

More generally, the UUIDv7 structure is as follows, with the first row being the number of bits:

# --- 48 ---   -- 4 --   - 12 -   -- 2 --   - 62 -
# unix_ts_ms | version | rand_a | variant | rand_b

In my alternative implementation #120830, rand_a is used for an additional 12-bit sub-millisecond precision constructed with Method 3, §6.2. This choice follows the recommendation of §5.7. Note that we are NOT allowed to have more than 12-bit of sub-ms precision according to the RFC since otherwise we would lose entropy guarantees.

On the other hand, rand_b is a 62-bit seeded counter generated according to Method 2, §6.2. The initial value of rand_b is a random 62-bit integer and is incremented by a random 32-bit integer if they are generated within the same timestamp tick. For instance:

# ms = 0
u0 = uuid7()  # u0.rand_b = random(62)
# ms = 1
u1 = uuid7()  # u1.rand_b = random(62)
# recall uuid7() within the same ms tick
u2 = uuid7()  # u2.rand_b = u1.rand_b + random(32)
# ms = 2
u3 = uuid7()  # u3.rand_b = random(62)

If rand_b overflows within the same ms tick, it is replaced by a uniformly distributed 62-bit integer and rand_a is advanced by 1 (this would emulate the fact that the UUID was in fact generated in the next sub-millisecond tick). In particular, the monotonicity is kept.

If rand_a also overflows, the entire routine is executed again (which would emulate the fact that some time has passed). Note that this could be possibly be used in side-channels since the execution of uuid7() would be twice slower. The technique of advancing rand_a in case of an overflow in rand_b tries to follow the RFC guidelines about counter rollover handling.

Actually, for this last point, I would like to discuss which direction is the best. Currently, it’s an “update-ahead” approach which allows to handle overflows in both the rand_a and rand_b parts. However, this might become disruptive in the sense that the exact sub-ms precision is actually lost. For instance, if I call uuid7() at some point and the timestamp that is being used would cause an overflow in rand_b and rand_a, the resulting UUID would be “as if” I called the function a bit later. Similarly, if I only have an overflow in rand_b, I advance rand_a by 1, namely I change the actual sub-millisecond component. I don’t have much experience in such corner cases in practice, so I would be happy if anyone could tell me whether this could cause serious issues.

Version 8

UUIDv8 is a versatile UUID because it has almost no constraints. It is quite close to UUIDv4, but does not need to be cryptographically secure. More precisely, UUIDv8 considers 122 free bits, split into three non-adjacent chunks of 48, 12 and 62 bits, as depicted on the following figure:

# -- 48 --   -- 4 --   -- 12 --   -- 2 --   -- 62 --
# custom_a | version | custom_b | variant | custom_c

The user might provide any of those three blocks separately. In #120650, those blocks are generated by default with random.getrandbits, which is quite efficient (in general, MT19937-based generators are faster than those targetting the uniform distribution). Since UUIDv8 is an implementation-detail, the RFC provides two alternatives:

  • Time-based UUIDv8: this one features a 10-ns precision with a 60-bit timestamp and 62 bits of random data. This could be used as an alternative to UUIDv7 lack of precision.
  • Name-based UUIDv8: this one allows the use of secure hashing algorithms such as SHA256/SHA-3/SHAKE-256 (see here for an example of SHA256-based UUIDv8).

I did not find explicit version 8 implementations that are not just simple implementations. However, I think that the standard library, if supporting v8, should either take the simplest implementation where monotonicity is no more guaranteed or use the implementations for time-based or name-based UUIDv8 suggested by the RFC. While I understand that there are packages already exposed, I think the role of the standard library is first and foremost to be standard and thus should follow the RFC as much as possible.

4 Likes

In light of the following comment on issue 89083, I am considering to switch to Method 1, §6.2 which is a Fixed Bit-Length Dedicated Counter. This would be quite distinct from Method 2 that I suggested but the rationale would be that it’s closer to what other implementations do.

In the end, I don’t want Python to have another method for generating UUIDs compared to other libraries. However, I missed the fact that the Rust implementation is actually a massively used library https://crates.io/crates/uuid/reverse_dependencies. In particular, I believe that their implementation would serve as a reference and thus I will go for a similar implementation of that library.

In addition, due to the fact that UUIDv7 are likely to be used for database storage, improving over UUIDv6, supporting timestamp offsets could be considered (see this discussion).

In summary, here’s what I would suggest for UUIDv7:

# --- 48 ---   -- 4 --   --- 12 ---   -- 2 --   --- 30 ---   - 32 -
# unix_ts_ms | version | counter_hi | variant | counter_lo | random
  • The counter (split into counter_hi and counter_lo) is initialized to a random 42-bit integer with MSB set to 0 (so it’s effectively a 41-bit random integer). Whenever a new UUIDv7 is generated within the same millisecond, the counter is incremented by 1.
  • If the counter overflows, we increment the timestamp and reset the counter to a random 42-bit integer with MSB set to 0.
  • The random part is always re-generated, independently of whether the counter was incremented or not.

The rationale of having the MSB set to 0 is given at the end of the paragraph Fixed Bit-Length Dedicated Counter Seeding:

For example, a 24-bit counter could have the 23 bits in least significant, rightmost position randomly initialized. The remaining most significant, leftmost counter bit is initialized as zero for the sole purpose of guarding against counter rollovers.

1 Like

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

Although i understand the rational about being compatible with PostgreSQL, that would make PostgreSQL behavior the “standard” instead of the spec, so i vote for milliseconds precission and add options to set how much extra subms bits to add.

I added an “other” choice (sorry I needed to delete and re-create the poll, but I assume that’s what you essentially wanted as well). AFAICT, what you want is still Method 3 but with a variable length for the subprecision. Because AFAIK, the RFC does not allow to mix a counter-based strategy with the subms approach.

1 Like

It’s a way to see It, method 3 with variable subprecision being default zero.

I don’t remember the details, i think RFC allows to mix subms with counters, or at least at some point i understood that. As i told you, the spec it’s a bit ambiguos how is It redacted, and i explained my conclusions in the GitHub thread.

At the very end of the method 3 description, the RFC says:

This technique can also be used in conjunction with one of the other methods, where this additional time precision would immediately follow the timestamp. Then, if any bits are to be used as a clock sequence, they would follow next.

Which means it allows for mixing counter-based with subms approach.

1 Like

But on the other hand, it also says (about the subms precision):

for UUIDv7, it is limited to 12 bits at maximum to reserve sufficient space for random bits.

So, if we use counters, the issue that we lose some of that randomness. And the RFC doesn’t tell me the minimum number of random bits.

Note that I’m not against a flexible implementation, I’m just trying to decide which road is the best for the standard library.

I’ve edited my previous post. There is no inconsistency with Method 1 and 3 as the subms timestamp would be considered as the embedded timestamp I think. So we can have 48 bits of timestamp, version bit, subms bits, variant bit, counter, randomness.

Section describing dedicated counter length also states that counter should be between 12 and 42 bits. I think we can safely assume 12 bits for subms and 30 bits for counter when combining both approaches. Of course we can shorten the subms part to make more room for the counter if needed, but limiting the whole section to 42 bits and ensuring the counter section having at least 12 bits seems like a safe option.

I’m voting for option 1, but with a note here.

No matter the construction method, without knowing the construction method of a v7 uuid, it’s unsafe to make assumptions about it. it should be possible to consume a uuid from either rust or postgres without any properties exposed that end up misleading by assuming the standard library’s construction method was used.

Truth is not established by voting. The question is very complex, and concerns whether it will be possible to ensure sub-millisecond precision and monotonicity when inserting records into a table with identifiers generated in different microservices. I have requested advice from the developer of the uuidv() function for PostgreSQL (Andrey Borodin). I suggest waiting for his answer.

1 Like

Andrey Borodin recommended Method 1 for Python, since Method 3 does not provide portability to platforms that do not have microsecond resolution system clocks.

1 Like

Truth is not established by voting

No indeed, but the main reason was to first gather users experience since I’m not well-versed in microservices and I don’t work with them in general. So I wanted to check the demand first.

since Method 3 does not provide portability to platforms that do not have microsecond resolution system clocks

I forgot about that. I think I’ll stay with my Method 1 then for a first iteration. We’ll see later during before the beta whether more feedback is given or not and if we need to expose more methods for UUIDv7.

3 Likes