UUID/GUID Versions Explained

"UUID" almost always means "v4, fully random" in practice — but that default is quietly expensive at scale, and as of RFC 9562 there's now a standardized alternative built for exactly this problem. Here's what each version actually encodes, and a byte-order quirk specific to .NET that breaks interop if you don't know it's there.

reference guide RFC 9562 .NET

// the versions

A UUID is 128 bits. Four bits of that encode a version number that tells you how the rest was generated — visible as the first character of the third group, e.g. the 4 in xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx. RFC 9562 (May 2024) is the current spec — it obsoletes RFC 4122 but doesn't invalidate anything: every v1/v3/v4/v5 UUID ever generated is still fully valid.

f47ac10b-58cc-4372-a567-0e02b2c3d479versionvariant
Anatomy of a UUID string: the leading character of the third group encodes the version — a 4 here means v4 (random). The leading bits of the fourth group encode the RFC variant, normally one of 8/9/a/b.
what each version encodes
v1timestamp + the generating machine's MAC address — leaks hardware identity, rarely used by choice today
v3name-based, MD5 — same namespace + name always produces the same UUID
v4fully random — the default; what Guid.NewGuid() and crypto.randomUUID() produce
v5name-based, SHA-1 — same idea as v3, preferred for its stronger collision resistance
v6 RFC 9562a reordered v1 — same timestamp+MAC data, rearranged to sort chronologically
v7 RFC 9562millisecond timestamp + random bits — sortable, no MAC leak, recommended for new systems
v8 RFC 9562custom/vendor-specific layout — the spec reserves the shape, you define what goes inside it
Nil (00000000-0000-0000-0000-000000000000) and Max (all fs) are also formally reserved values as of RFC 9562 — useful as an explicit "no value"/"unbounded" sentinel instead of null, in systems where the column type disallows nulls.

// why v7 exists: random UUIDs quietly wreck index performance

A v4 UUID as a primary key lands at a random position in a B-tree index every single insert. On a small table that's invisible. At millions of rows, every insert forces a random page read, and pages that fill up have to split — over time the index fills with half-empty pages, fragments badly, and range scans degrade into random I/O instead of sequential reads.

measured impact (published benchmarks)
Insert throughputv4 primary keys cost 30–50% versus a sequential key
50M-row bulk insert~20 minutes with v4 vs. ~1.8 minutes with v7 in one published PostgreSQL benchmark
Index sizeroughly 25% smaller with v7 in the same benchmark — less fragmentation, denser pages

v7's timestamp-first layout means new rows' keys cluster at the end of the index instead of scattering everywhere, which is exactly what a B-tree wants for insert-heavy workloads. If you're choosing a UUID primary key for a new table today, v7 is very likely the better default over v4 — you keep the "no coordination needed to avoid collisions" property of UUIDs while getting most of the locality benefit of an auto-incrementing integer.

v4 — random position, every insert123456next insert can land anywherev7 — always appends at the growing edge123456next insert always lands past the last one
Where six successive inserts land in a primary-key index. A v4 (random) UUID can land anywhere on every insert — forcing page splits and fragmentation as the table grows. A v7 (time-ordered) UUID always lands past the previous one, the same locality an auto-incrementing integer gives you.

// name-based UUIDs (v3/v5) — when determinism is the point

Unlike v4/v7, v3 and v5 aren't random — they're a hash of a namespace UUID plus a name string, so the same two inputs always produce the same UUID, anywhere, with no shared state or database lookup required. Useful for deriving a stable ID from something you already have — e.g. turning a known external key (an email, a DNS name, a legacy string ID) into a UUID-shaped identifier deterministically, so the same input never produces two different IDs across services.

// "GUID" vs "UUID" — and the .NET byte-order trap

GUID is Microsoft's name for the same 128-bit format — modern Guid.NewGuid() produces a standard RFC-compliant v4 UUID, so the terms are interchangeable in practice today. The string representation matches the RFC exactly. Where it diverges is the raw bytes.

Guid.ToByteArray() does not return RFC byte order. .NET stores a Guid's first three groups (Data1, Data2, Data3) in little-endian order internally, while the RFC specifies big-endian (network byte order) throughout. The string form looks identical either way — the mismatch only appears once you serialize to raw bytes and compare against a non-.NET system (Java, Postgres's native uuid type, a byte-level API contract). .NET 8 added ToByteArray(bigEndian: true) specifically to produce RFC-correct bytes — worth using explicitly any time a GUID's byte representation crosses a system boundary, not just its string form.

// try it yourself

Generate a random (v4) GUID, or paste one you already have, and see it in every format .NET's Guid.ToString() supports — N, D, B, P, and the X constructor form — with the GUID Generator & Format Converter.