UUID v7 in .NET: the byte-order pitfall

How Guid.ToByteArray() silently scrambles v7's sortability, why it bloats your index by ~35%, and the one-character fix.

By uniqueid.tech — an independent developer based in New Zealand Updated 1 June 2026

.NET 9 added native UUID v7 support via Guid.CreateVersion7(). The implementation produces RFC 9562-compliant UUIDs when serialised to their canonical text form. There is, however, a gotcha that can quietly defeat the entire point of choosing v7 in the first place: Guid.ToByteArray() returns bytes in .NET's native little-endian layout, not the big-endian RFC byte order. If you persist those bytes to a database or any other system that sorts identifiers as binary, your supposedly sequential UUIDs will not sort sequentially.

This article explains the problem, why it exists, and how to fix it. It assumes you've already chosen UUIDs as your key type; if that decision is still open, see UUID vs auto-increment primary keys for the background.

The symptom

Generate a v7 UUID, look at its string form, then look at its byte array:

var guid = Guid.CreateVersion7();
Console.WriteLine(guid.ToString());
// "0199ed42-d130-7a4f-b8b2-3c8e5e3f8a91"

Console.WriteLine(Convert.ToHexString(guid.ToByteArray()).ToLower());
// "42ed990130d14f7ab8b23c8e5e3f8a91"

The first 8 hex characters of the string are 0199ed42. The first 8 hex characters of the byte array are 42ed9901 — the same bytes, reversed. The next four bytes are also reversed (d130 becomes 30d1), and so are the four after that (7a4f becomes 4f7a). The trailing 16 hex characters match.

The string form is correct. The byte form is wrong — at least, wrong if anything downstream is going to compare these bytes as sortable sequences.

Why it happens

The .NET Guid type predates UUID standardisation in any practical sense. Its in-memory layout matches the Microsoft GUID structure used in COM and Windows APIs, which laid out the first three fields as native-integer types (a 32-bit int, then two 16-bit shorts) followed by an 8-byte array.

On x86 and x64 — the only architectures most .NET code will ever run on — those native integers are stored little-endian. When ToByteArray() was added, it returned bytes in memory order, which means the first three fields appear with their bytes reversed relative to the RFC's big-endian byte order.

For UUID v4 this is invisible: the bytes are random, the field boundaries don't matter, and round-tripping through Guid.ToByteArray() and new Guid(bytes) is consistent. The string form is generated from the in-memory representation in a way that produces the canonical RFC-ordered output, so as long as nothing outside .NET ever looks at the raw bytes, the divergence is harmless.

For UUID v7 it matters intensely. v7's defining property is that the high bytes encode the timestamp, in order, so two UUIDs generated a millisecond apart compare correctly when sorted as binary. If the bytes are written to storage in the wrong order, the timestamp bits scatter across the value in a way that has no useful sort property at all.

The fix

.NET 8 added an overload that produces RFC byte order directly:

var guid = Guid.CreateVersion7();

Span<byte> rfcBytes = stackalloc byte[16];
guid.TryWriteBytes(rfcBytes, bigEndian: true, out _);

Console.WriteLine(Convert.ToHexString(rfcBytes).ToLower());
// "0199ed42d1307a4fb8b23c8e5e3f8a91" — matches the string form

There's a matching constructor for round-tripping:

var rebuilt = new Guid(rfcBytes, bigEndian: true);
Console.WriteLine(rebuilt == guid);  // True

The rule is straightforward: at any boundary where the bytes will be compared, sorted, or interpreted outside .NET, use the bigEndian: true overloads. Inside .NET — passing a Guid between methods, storing in collections, serialising as a string — the default behaviour is fine.

Why this matters for database indexes

The whole purpose of UUID v7 over v4 is to play well with B-tree indexes, which back the primary key on most relational databases. A B-tree of randomly-distributed keys suffers from frequent page splits: each new insert hits a random page, that page fills up, the database splits it, and over time the index becomes a sparse, fragmented structure with poor cache locality and bloated storage.

v7 fixes this by making each new insert land at the tail of the index, where the previous insert was. Page splits drop to near zero. Storage grows tightly. Range scans by creation time become cheap.

But this only works if the database sees the v7 UUIDs in their RFC byte order, where the timestamp leads. If the database sees them in .NET's little-endian-first-three-fields layout, the timestamp bytes are scattered through the middle of the value, the values appear effectively random from the index's perspective, and you've gone to the trouble of using v7 for no benefit.

A comparison run inserting 100,000 v7 UUIDs as a primary key into PostgreSQL produced an index roughly 35% larger when bytes were written in .NET's native order versus RFC order. On a high-write production table that's both a storage cost and a sustained performance cost.

Database-specific notes

PostgreSQL

The uuid type stores 16 bytes in RFC order and sorts lexicographically by those bytes. Use bigEndian: true when binding parameters or writing the value, and v7's sort property is preserved. Most ADO.NET drivers handle this conversion correctly if you pass Guid values directly, but verify with your specific driver — some have historical quirks worth confirming via a small test before relying on them. For a fuller treatment of v7 on Postgres — generation options, index density, and gotchas — see UUID v7 in PostgreSQL.

SQL Server

The uniqueidentifier type uses yet another byte order, and SQL Server's sort order on uniqueidentifier is genuinely peculiar — it doesn't sort by any of the obvious orderings. For v7 UUIDs in SQL Server, a common pattern is to store them in a BINARY(16) column in RFC order rather than as uniqueidentifier. This costs you the type-level semantics but preserves sortability. An alternative is to store as CHAR(36) and accept the storage overhead in exchange for a sortable, readable column.

SQLite

No native UUID type. Store as BLOB (16 bytes, RFC order) for compact and sortable, or as TEXT for readability.

MySQL and MariaDB

Store v7 UUIDs as BINARY(16) in RFC byte order. The UUID_TO_BIN() function has a swap_flag parameter that rearranges bytes for older time-based UUIDs (v1 style) — leave it off for v7, which is already in the right order natively.

Summary

  • Guid.CreateVersion7() produces RFC-compliant v7 UUIDs.
  • .ToString() produces the correct canonical text form.
  • .ToByteArray() produces the native little-endian layout, which breaks v7's sortability if persisted as binary.
  • Use TryWriteBytes(span, bigEndian: true, out _) and new Guid(span, bigEndian: true) when reading or writing v7 bytes at any boundary that compares them as sortable sequences.
  • For SQL Server, consider BINARY(16) over uniqueidentifier to preserve sortability.
  • For PostgreSQL, MySQL, MariaDB, and SQLite, write RFC byte order and the database will sort correctly.

The byte-order issue is a one-line fix once you know about it and a silent index-bloat bug if you don't. If your team is migrating to UUID v7 expecting database benefits, audit every code path that serialises Guid to bytes — there's a good chance at least one of them needs bigEndian: true added.

The same bigEndian: true fix applies to UUID v6, which is also a time-ordered layout whose sortability depends on RFC byte order. Try the UUID v7 generator to see the canonical string form, read more about the wider UUID family in UUID versions explained, or step back to the broader picture in generating IDs in distributed systems.

An unhandled error has occurred. Reload ×