Benchmarking UUIDv7

Louis Laugesen ยท December 18, 2025

I wanted to replicate Stripe's name prefixed ID's for some of my projects and while looking around for existing implementations I found the jetify-com/typeid-go package was a good place to start.

But when working on implementing my own version of Stripe's ID's I noticed something funny about the UUID generation: Internally, it was using gofrs/uuid for generating UUID's instead of what I would typically use: google/uuid.

And since I am creating a lot of these id's in one of my projects I was curious if there was any kind of performance impact picking one over the other.

And if we're going to benchmark these two, we might as well see if there are any others worth looking into too.

Turns out there's a few UUID generators in Go to choose from:

Ok, so how do these differ?

The Benchmark

To compare the libraries, I created some benchmark tests. The benchmarks measure UUIDv7 generation in tight loops and compare generation only.

Results

Running go test -bench=. -benchmem on my development machine:

~/uuidbench go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: github.com/lsl/uuidbench
cpu: 13th Gen Intel(R) Core(TM) i7-13800H
BenchmarkCoolAJV7-20            14051618                87.92 ns/op           16 B/op          1 allocs/op
BenchmarkGofrsV7-20             11589558               104.1 ns/op            16 B/op          1 allocs/op
BenchmarkGoogleV7-20             8635333               135.0 ns/op            16 B/op          1 allocs/op
BenchmarkToolboxV7-20            2825163               441.4 ns/op           216 B/op          7 allocs/op
BenchmarkUUID6ProtoV7-20          745435              1603 ns/op               8 B/op          1 allocs/op

The top three (coolaj86, gofrs, google) all allocate the same 16 bytes per UUID with a single allocation. Toolbox and uuid6-proto are slower, with toolbox also allocating much more memory per UUID.

Implementation Details

Given the implementations

coolaj86/uuidv7

The fastest implementation (87.92 ns/op). The code pre-allocates a 96-byte buffer (enough for 10 UUIDs) and only calls rand.Read() when the buffer is exhausted, rather than calling rand.Read() for every single UUID like other implementations. Uses a mutex for thread-safety.

The shared buffer provides all the random bytes for the UUID (bytes 6-15, with bytes 0-5 overwritten by timestamp). If you generate more than ~9 UUIDs in a single millisecond there could theoretically be a uniquness concern. However, the randomness of rand.Read() makes this astronomically unlikely.

The preloading a buffer strategy seems to be what gives this implementation its edge.

gofrs/uuid

Uses a mutex for counter and timestamp tracking. Uses a Gen struct that maintains the last timestamp and sequence counter, allowing you to create isolated generators per goroutine or share one across goroutines with proper synchronization.

Uses UnixMilli() directly to get the timestamp, then increments a counter if the time hasn't changed since the last UUID generation. The mutex ensures atomic updates to the counter when generating multiple UUIDs within the same millisecond, preventing collisions.

Calls the random reader for each UUID individually, which seems to be what adds the overhead compared to coolaj86's implementation.

google/uuid

Package-level state with a global mutex for thread-safety. Uses the mutex to coordinate access to shared timestamp and counter state across all goroutines.

Generating a new UUID involves a NewRandom() call first to generate a full random UUID, then modifies it with makeV7().

makeV7() uses UnixNano() to perform a division (nano / nanoPerMilli) to extract milliseconds, then uses the fractional nanoseconds to create a sub-millisecond sequence number (seq = (nano - milli*nanoPerMilli) >> 8). This provides up to ~3906 values per millisecond automatically from the sub-millisecond precision.

If you generate more than ~3906 UUIDs in the same millisecond (unlikely), it increments the combined value (milli<<12 + seq) and extracts the new sequence from that. However, the sequence field is still limited to 12 bits (0-4095), so you can only generate up to 4096 UUIDs per millisecond total anyway. This is more complex than gofrs's approach of using UnixMilli() directly with a simple counter increment, and both are ultimately limited to ~4096 UUIDs per millisecond by the 12-bit sequence field in the UUIDv7 spec, so the added complexity doesn't seem to provide a meaningful advantage. I may be misunderstanding the purpose of this approach here.

The Decision

The performance differences between the top three performing libraries (coolaj86, gofrs, google) are negligible: in practice we're talking about 10-30 nanoseconds per UUID here and even that is questionable given the discovery that we're really just benchmarking how well we're calling rand.Read() at this point.

I had originally chosen google/uuid because it was the UUID library I've always used but seeing the counter logic and slightly worse benchmark performance I decided to switch to using gofrs/uuid.

If you're wondering why not the coolaj86 version purely due to the benchmark performance: I don't put a lot of stock in benchmarks like these, they're a great tool and provide a signal about where to look but most code won't be generating thousands upon thousands of uuids in tight loop.

However, I do think coolaj86's buffer-based approach makes a lot of sense and I'm wondering if the buffer approach paired with a counter could be an even further improvement.

Update Jan 2026

I decided to try this out and the result was better than expected. Code available @ lsl/uuid.

~/uuidbench go test ./... -bench=. -benchmem -benchtime=3s
goos: linux
goarch: amd64
pkg: github.com/lsl/uuidbench
cpu: 13th Gen Intel(R) Core(TM) i7-13800H
BenchmarkGofrsV7-20             34792820               102.9 ns/op            16 B/op          1 allocs/op
BenchmarkGoogleV7-20            26217514               130.9 ns/op            16 B/op          1 allocs/op
BenchmarkToolboxV7-20            8009846               446.0 ns/op           216 B/op          7 allocs/op
BenchmarkUUID6ProtoV7-20         2243642              1602 ns/op               8 B/op          1 allocs/op
BenchmarkCoolAJV7-20            41139253                86.47 ns/op           16 B/op          1 allocs/op
BenchmarkLSLV7-20               53252844                65.29 ns/op            0 B/op          0 allocs/op
PASS
ok      github.com/lsl/uuidbench        25.976s