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 _)andnew 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)overuniqueidentifierto 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.