UUID v7

Comic-style comparative illustration: on the left, Jorge with UUID-V4 trapped on a tangled, chaotic track; on the right, Jorge with UUID-V7 advancing along a tidy, straight athletics track.

Table of contents


My Problem πŸ€”

In my project I use PostgreSQL as the database and UUID as the identifier on every model. It’s the usual standard: universal, collision-free, secure. But I ran into a problem when my tables started to grow.

UUID v4 is fully random. That means each new record is inserted at a random position inside the B-Tree index. The index becomes fragmented, pages split, and write performance degrades progressively. On tables with millions of records, the impact is real.

The solution has existed for a long time in databases like MongoDB with ObjectID or in systems that use ULID: embed the timestamp into the identifier so new records always get inserted at the end of the index.

UUID v7, defined in RFC 9562, brings exactly that to the UUID standard. PostgreSQL 18 already includes the native uuidv7() function. But Swift β€” and by extension Fluent β€” still generates UUID v4 by default with UUID().

If the identifier is generated by my application before persisting it, I’m losing the v7 benefits even though my database already supports it.


My Solution 🧩

I decided to create an extension on UUID that implements UUID v7 generation directly in Swift, following the RFC 9562 specification.

Monotonic state

The first thing I need is a shared state that guarantees two UUIDs generated within the same millisecond are distinct and ordered. I use a Mutex from the Synchronization framework to make it thread-safe without resorting to DispatchQueue or actor:

import Foundation
import Synchronization

private let v7State = Mutex(V7State())

private struct V7State: Sendable {
    var lastTimestamp: UInt64 = 0
    var sequence: UInt16 = 0
}

UUID generation

The main extension implements the static method v7() that generates a UUID compliant with RFC 9562:

extension UUID {
    public static func v7() -> UUID {
        var bytes = (
            UInt8(0), UInt8(0), UInt8(0), UInt8(0),
            UInt8(0), UInt8(0), UInt8(0), UInt8(0),
            UInt8(0), UInt8(0), UInt8(0), UInt8(0),
            UInt8(0), UInt8(0), UInt8(0), UInt8(0)
        )

        let (timestamp, seq) = v7State.withLock { state -> (UInt64, UInt16) in
            var now = UInt64(Date().timeIntervalSince1970 * 1000)

            if now == state.lastTimestamp {
                if state.sequence >= 0x0FFF {
                    while now == state.lastTimestamp {
                        now = UInt64(Date().timeIntervalSince1970 * 1000)
                    }
                    state.lastTimestamp = now
                    state.sequence = UInt16.random(in: 0...0x0FFF)
                } else {
                    state.sequence += 1
                }
            } else {
                state.lastTimestamp = now
                state.sequence = UInt16.random(in: 0...0x0FFF)
            }

            return (now, state.sequence)
        }

        // Bytes 0–5: 48-bit timestamp (big-endian)
        bytes.0 = UInt8(truncatingIfNeeded: timestamp >> 40)
        bytes.1 = UInt8(truncatingIfNeeded: timestamp >> 32)
        bytes.2 = UInt8(truncatingIfNeeded: timestamp >> 24)
        bytes.3 = UInt8(truncatingIfNeeded: timestamp >> 16)
        bytes.4 = UInt8(truncatingIfNeeded: timestamp >> 8)
        bytes.5 = UInt8(truncatingIfNeeded: timestamp)

        // Byte 6: version (0x7_) | top 4 bits of sequence
        bytes.6 = 0x70 | UInt8(seq >> 8)

        // Byte 7: lower 8 bits of sequence
        bytes.7 = UInt8(truncatingIfNeeded: seq)

        // Bytes 8–15: random, then set variant
        var random: UInt64 = 0
        withUnsafeMutableBytes(of: &random) { buf in
            _ = SecRandomCopyBytes(kSecRandomDefault, buf.count, buf.baseAddress!)
        }
        bytes.8  = UInt8(truncatingIfNeeded: random >> 56)
        bytes.9  = UInt8(truncatingIfNeeded: random >> 48)
        bytes.10 = UInt8(truncatingIfNeeded: random >> 40)
        bytes.11 = UInt8(truncatingIfNeeded: random >> 32)
        bytes.12 = UInt8(truncatingIfNeeded: random >> 24)
        bytes.13 = UInt8(truncatingIfNeeded: random >> 16)
        bytes.14 = UInt8(truncatingIfNeeded: random >> 8)
        bytes.15 = UInt8(truncatingIfNeeded: random)

        // Byte 8: variant 0b10xx_xxxx
        bytes.8 = (bytes.8 & 0x3F) | 0x80

        return UUID(uuid: bytes)
    }
}

The structure of the generated UUID respects the RFC 9562 specification:

  • Bytes 0–5 (48 bits): timestamp in milliseconds since epoch, big-endian. This is what guarantees chronological order.
  • Byte 6 (high nibble): version 0x7 β€” identifies the UUID as v7.
  • Bytes 6–7 (lower 12 bits): sequence counter. Guarantees monotonicity within the same millisecond.
  • Bytes 8–15 (64 bits): random, generated with SecRandomCopyBytes, with the two highest bits of byte 8 forced to 0b10 to mark the RFC 4122 variant.

The monotonic logic inside withLock works like this: when two calls happen in the same millisecond, the counter is incremented. If the counter reaches the maximum (0x0FFF, that is 4095 UUIDs in a single millisecond), the implementation actively waits for the next millisecond before continuing β€” it never generates two identical UUIDs.

Inspection utilities

In addition to generation, I added two properties to inspect any UUID: one that checks whether it is v7 and another that extracts the embedded timestamp:

extension UUID {
    public var isV7: Bool {
        (uuid.6 >> 4) == 0x07 && (uuid.8 >> 6) == 0x02
    }

    public var v7Timestamp: Date? {
        guard isV7 else { return nil }

        let ms = UInt64(uuid.0) << 40
            | UInt64(uuid.1) << 32
            | UInt64(uuid.2) << 24
            | UInt64(uuid.3) << 16
            | UInt64(uuid.4) << 8
            | UInt64(uuid.5)

        return Date(timeIntervalSince1970: Double(ms) / 1000.0)
    }
}

isV7 verifies that the high nibble of byte 6 is 0x07 (version 7) and that the two highest bits of byte 8 are 0b10 (RFC 4122 variant). v7Timestamp reconstructs the 48 bits of the timestamp from bytes 0–5 and converts them into a Date.


My Result 🎯

Adoption is immediate. In any Fluent model, all I have to do is replace UUID() with UUID.v7() in the initializer:

// Before
self.id = UUID()

// After
self.id = UUID.v7()

The database type does not change β€” it’s still UUID. Fluent needs no modification. The change is completely transparent to the ORM.

The benefits I got are direct:

  • Ordered indexes β€” new records are always inserted at the end of the B-Tree, eliminating fragmentation
  • Native chronological ordering β€” ORDER BY id is equivalent to ORDER BY created_at without an extra column
  • Embedded timestamp β€” I can extract when a record was created directly from the UUID with v7Timestamp
  • Backward compatible β€” the format is still standard UUID; any system that accepts UUID v4 accepts v7
  • Thread-safe β€” works correctly from multiple concurrent threads thanks to the Mutex
let id = UUID.v7()

print(id.isV7)          // true
print(id.v7Timestamp!)  // the creation date embedded in the UUID

And if you’re already on PostgreSQL 18, the native uuidv7() function generates UUIDs with the same structure. UUIDs generated from Swift and from PostgreSQL are interchangeable and comparable to each other.

The change from UUID() to UUID.v7() is a one-liner. The advantages, permanent.

Keep coding, keep running πŸƒβ€β™‚οΈ