Time-based Universally Unique Identifiers (UUIDs v7)

Universally Unique Identifiers (UUID for short) are 128-bit numbers with a high degree of unicity so that the possibility of collision is extremely low. They are used for various purposes such as identifying resources, correlation IDs in logging and tracing, primary keys in databases, and more. UUIDs are known as GUIDs (Globally Unique Identifiers) on Windows systems. Several version have been defined over time, version 7 being the latest of them, designed to solve some problems present in the other version.

What is a UUID

A UUID is a 128-bit number that is typically formatted as 36 character hexadecimal string with hyphens. It’s defined by the RFC 4122 standard (which overseeded the now obsolete RFC 9562). A formatted UUID looks like this:

D5501097-8306-4009-8532-715DB2C65607

The general format is

xxxxxxxx-xxxx-Vxxx-Rxxxx-xxxxxxxxxxxx

In this representation

  • x is a hexadecimal character
  • V is the version
  • R is the variant (10xx for RFC 4122 compliant UUIDs)

UUID variants

There are multiple variants for UUIDs. He is a table with a short description:

VariantMethodDescription
1Timestamp + MAC addressReveals device information (MAC address)
3Name-based (MD5 hash)Generated by hashing a namespace + name pair, using MD5 which is cryptographically broken.
4Random122 random bits
5Name-based (SHA-1 hash)Generated by hashing a namespace + name pair, using SHA-1 is considered obsolete
7Time-based + randomOptimized for use in databases
8CustomExperimental, reserved for application-specific formats

The older versions of UUID had different problems:

  • versions 3 and 5 rely on cryptographically broken or obsolete hashes, and shouldn’t be used where uniqueness or secrecy depend of cryptographical strength
  • version 1 embeds the MAC address of the device on which it was created as well as the timestamp when it was created which means the location and time of creating can be tracked; moreover, UUIDs may collide because VM or contains may share the same MAC address
  • version 4 are purely random, which means they are completely out of order, which makes them inappropriate for indexed database columns; however, they are very good for cases where you need simple unique IDs without carrying about ordering or readability

Enter version 7

The UUID v7 was designed to provide better performance when using UUIDs in databases (especially for primary keys). It consists of two main parts:

  • the most significant 48 bits are the Unix timestamp of the moment when it was created, with millisecond resolution
  • the rest of the bits (except the 4 bit for version and 2 for the variant) are completely random (just like for version 4)

This means this UUIDs are ordered chronologically while still retaining a high degree of uniqueness, which makes them more index-friendly.

Here is, for instance, an article that attempts to answer the question “Is UUIDv7 actually better than UUIDv4 in PostgreSQL?“. The conclusion, reproduced below, is that both insert times and index size are significantly better:

Results for benchmarking UUID v4 and v7 for PostgresSQL.

Generating UUIDs v7

Windows API

There is no support in the native API for creating v7 UUIDs. UuidCreate(), as well as CoCreateGui() that is basically just a wrapper over UuidCreate, still generate only v4 UUIDs.

Linux

It’s the same situation, the uuid_generate() API (from <uuid/uuid.h>) only generates v4 (although libuuid also supports creating v1 UUIDs).

C++

There is no standard library function for creating UUIDs, but we can write such an implementation, that works on all platforms.

using uuid_t = std::array<uint8_t, 16>;

uuid_t generate_uuidv7()
{
   uuid_t bytes{};

   // get current Unix timestamp in milliseconds
   auto now = std::chrono::system_clock::now();
   uint64_t ms_since_epoch =
      std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();

   // fill with random bytes
   std::random_device rd;
   std::mt19937_64 gen(rd());
   std::uniform_int_distribution<uint64_t> dist;
   uint64_t rand_hi = dist(gen);
   uint64_t rand_lo = dist(gen);

   for (int i = 0; i < 8; ++i) bytes[i] = (rand_hi >> ((7 - i) * 8)) & 0xFF;
   for (int i = 0; i < 8; ++i) bytes[8 + i] = (rand_lo >> ((7 - i) * 8)) & 0xFF;

   // overwrite the first 6 bytes with timestamp (big-endian)
   bytes[0] = (ms_since_epoch >> 40) & 0xFF;
   bytes[1] = (ms_since_epoch >> 32) & 0xFF;
   bytes[2] = (ms_since_epoch >> 24) & 0xFF;
   bytes[3] = (ms_since_epoch >> 16) & 0xFF;
   bytes[4] = (ms_since_epoch >> 8) & 0xFF;
   bytes[5] = (ms_since_epoch) & 0xFF;

   // set version (UUIDv7 = 0b0111)
   bytes[6] = (bytes[6] & 0x0F) | 0x70;

   // set variant (RFC 4122)
   bytes[8] = (bytes[8] & 0x3F) | 0x80;

   return bytes;
}

std::string to_string(const uuid_t& uuid)
{
   return std::format(
      "{:02x}{:02x}{:02x}{:02x}-"
      "{:02x}{:02x}-"
      "{:02x}{:02x}-"
      "{:02x}{:02x}-"
      "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
      uuid[0], uuid[1], uuid[2], uuid[3],
      uuid[4], uuid[5],
      uuid[6], uuid[7],
      uuid[8], uuid[9],
      uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]
   );
}

This could be used as follows:

int main()
{
   for (int i = 0; i < 5; ++i)
   {
      auto uuid = generate_uuidv7();
      std::println("{}", to_string(uuid));
   }
}

.NET

Starting with .NET 9, you can use the Guid.CreateVersion7() method, as shown below:

var guid = Guid.CreateVersion7();
Console.WriteLine(guid);

This function has an overload and you can also pass a DateTimeOffset to it:

var guid = Guid.CreateVersion7(TimeProvider.System.GetUtcNow());

For previous versions of .NET, you’d have to rely on 3rd party NuGet package.

Read more

You can read more about UUIDs v7 on this articles:

1 thought on “Time-based Universally Unique Identifiers (UUIDs v7)”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.