---
title: 4096x more precision in GUID v7
date: 2026-03-24
tags: [.NET]
---

In .NET, `Guid.CreateVersion7()` ignores the optional sub-millisecond precision allowed for by RFC 9562. This post details how to increase the maximum precision from 1ms to 2.44140625ns, maintaining a more fine-grained order within the same millisecond. And, of course, with zero allocations to the managed heap.

## A note on entropy   

RFC 9562 §6.9 states that "Implementations **SHOULD** utilize a cryptographically secure pseudorandom number generator". 

Some investigation into the file `src/runtime/src/libraries/System.Private.CoreLib/src/System/Guid.cs` in the .NET monorepo shows that under the hood, `Guid.CreateVersion7()` wraps `Guid.NewGuid()`, which farms out responsibility for the CSPRNG to the system. 

To keep this security, we'll wrap and modify the existing GUID factory methods, instead of populating the random bits ourslves. 

## Setting the millisecond-precision timestamp 

The first point of business is encoding the timestamp with millisecond-level precision. 

With .NET 9.0 and greater, we can farm this out to the existing `CreateVersion7()`.

```csharp
Span<byte> bytes = stackalloc byte[16];
_ = Guid.CreateVersion7(d).TryWriteBytes(bytes);
```

>[!NOTE]
> We don't expect `TryWriteBytes()` to fail here. As of time of writing, the only reason it would fail in any version of .NET is if `bytes` didn't have enough space, but we know that `Guid.NewGuid()` and `Guid.CreateVersion7()` will always be exactly 16 bytes.

If using between .NET Core 2.1 and .NET 8.x, or .NET Standard 2.1, this has to be done manually. The below example demonstrates the appropriate mapping in case you're subject to this limitation.  

```csharp
Span<byte> bytes = stackalloc byte[16];
_ = Guid.NewGuid().TryWriteBytes(bytes);

long totalMilliseconds = d.ToUnixTimeMilliseconds();
bytes[3] = (byte)(totalMilliseconds >> 0x28);
bytes[2] = (byte)(totalMilliseconds >> 0x20);
bytes[1] = (byte)(totalMilliseconds >> 0x18);
bytes[0] = (byte)(totalMilliseconds >> 0x10);
bytes[5] = (byte)(totalMilliseconds >> 0x08);
bytes[4] = (byte)(totalMilliseconds);
```

All other versions (.NET Core &le; 2.0, .NET Standard &le; 2.0, .NET Framework) also don't support `Guid.TryWriteBytes()`. Instead, use `.ToByteArray()` instead, which will cause a heap allocation of 2352 bytes. 

```csharp
byte[] bytes = Guid.NewGuid().ToByteArray();

long totalMilliseconds = d.ToUnixTimeMilliseconds();
bytes[3] = (byte)(totalMilliseconds >> 0x28);
bytes[2] = (byte)(totalMilliseconds >> 0x20);
bytes[1] = (byte)(totalMilliseconds >> 0x18);
bytes[0] = (byte)(totalMilliseconds >> 0x10);
bytes[5] = (byte)(totalMilliseconds >> 0x08);
bytes[4] = (byte)(totalMilliseconds);
```

## Setting the fractional component 

Next up, we need to compute the fraction of a millisecond that the remaining ticks represent. While there are 10,000 ticks in a millisecond, GUID v7 has a maximum precision of 1/4096 of a millisecond (equal to 2.44140625ns). 

Mathematically speaking, the denominator of this fraction is 4096, and the numerator is defined as: 

$$
n = {{min(\lfloor{{({\Delta_{ticks}\cdot4096}) + 5000}\over10000}\rfloor, 4095)}}
$$

In code, we can express that like so:

```csharp
int fracNumerator = (int) Math.Min((d.Ticks % 10000 * 4096 + 5000) / 10_000, 4095);
```

Then, we can update our Guid like so: 

```csharp
bytes[6] = (byte)(fracNumerator & 0xFF);
bytes[7] = (byte)((fracNumerator >> 8) & 0x0F);
```

## Repairing the version 

Finally, since we've just overwritten the 8th byte (which is shared between the version number and the fractional component), we need to put its first nibble back to 7. We can do this with some simple bit logic.

```csharp
bytes[7] = (byte)((bytes[7] & 0x0F) | 0x70);
```

## Complete method 

Putting all this together, we can create a method for creating a GUID v7 that's about 500x more precise than the standard implementation in .NET 9: 

```csharp
static Guid CreateVersion7Precise(DateTimeOffset d)
{
#if NET9_0_OR_GREATER
    Span<byte> bytes = stackalloc byte[16];
    Guid.CreateVersion7(d).TryWriteBytes(bytes);
#elif NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
    Span<byte> bytes = stackalloc byte[16];
    Guid.NewGuid().TryWriteBytes(bytes);

    long totalMilliseconds = d.ToUnixTimeMilliseconds();
    bytes[3] = (byte)(totalMilliseconds >> 0x28);
    bytes[2] = (byte)(totalMilliseconds >> 0x20);
    bytes[1] = (byte)(totalMilliseconds >> 0x18);
    bytes[0] = (byte)(totalMilliseconds >> 0x10);
    bytes[5] = (byte)(totalMilliseconds >> 0x08);
    bytes[4] = (byte)(totalMilliseconds);
#else
    byte[] bytes = Guid.NewGuid().ToByteArray();

    long totalMilliseconds = d.ToUnixTimeMilliseconds();
    bytes[3] = (byte)(totalMilliseconds >> 0x28);
    bytes[2] = (byte)(totalMilliseconds >> 0x20);
    bytes[1] = (byte)(totalMilliseconds >> 0x18);
    bytes[0] = (byte)(totalMilliseconds >> 0x10);
    bytes[5] = (byte)(totalMilliseconds >> 0x08);
    bytes[4] = (byte)(totalMilliseconds);
#endif

    int fracNumerator = (int) Math.Min((d.Ticks % 10000 * 4096 + 5000) / 10_000, 4095);
    bytes[6] = (byte)(fracNumerator & 0xFF);
    bytes[7] = (byte)((fracNumerator >> 8) & 0x0F);

    bytes[7] = (byte)((bytes[7] & 0x0F) | 0x70);

    return new Guid(bytes);
}
```

## Testing consideration

Since GUID v7 can't express the full range of ticks, this method has a margin of error of $\pm2$ ticks, or $\pm200ns$.When testing, the success condition is: 

$$
|ticks_1 - ticks_2| \le 2
$$

Or, in code: 

```csharp
Math.Abs(left.Ticks - right.Ticks) <= 2;
```