UUIDs: Why v4 Is Random, v7 Is Sorted, and When You Should Use Neither

A UUID is 128 bits with a few of them doing very specific jobs, and which version you pick quietly shapes your database performance and your security posture. This guide reads the bit layout, works through v4 collision math, explains why random primary keys wreck B+tree indexes, and shows when auto-increment or ULID is the better answer.

The UUID is the most common identifier nobody reads. Teams pick "UUID" as a column type, some library hands them version 4, and five years later the primary key index is twice the size it should be and inserts are slow — all decisions that were made implicitly, by a default. There is also a genuine design question hiding in those 36 characters: should your identifier be random, or should it be sorted by time? The answer changes how your database behaves under load. This article opens the format up and makes the choice explicit.

The 128-bit layout

A UUID is 128 bits — 2^128 possible values, roughly 3.4 × 10^38 — conventionally written as 32 hex digits in groups of 8-4-4-4-12. The hyphens are cosmetic; the bits are not free. Two small fields are fixed in every UUID:

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

M = version nibble   (1, 3, 4, 5, 6, 7, or 8)
N = variant nibble  (8, 9, a, or b = RFC 4122)

The first hex digit of the third group is the version; the first digit of the fourth group carries the variant bits, which in modern UUIDs are fixed to binary 10. That is why you can identify a v4 UUID by eye: a 4 in the middle and an 8, 9, a or b right after it. It is also why a "random" v4 UUID only has 122 random bits, not 128 — six bits are reserved for these labels.

v4: 122 random bits, and the collision math

Version 4 is pure randomness: fill the remaining 122 bits from a good random source and you are done, with no coordination, no central authority, and no knowledge of anyone else's IDs. Uniqueness is then a probability question, answered by the birthday bound: the chance of a collision grows with roughly the square of the number of IDs generated, over the size of the space. Concretely: one million v4 UUIDs give a collision probability around 10^-25. You need on the order of 2^61 — about 2.3 quintillion — UUIDs before the odds reach even. Generating one billion per second, nonstop, for more than seventy years.

So accidental collisions are not a real operational concern. The real concerns are different: a 36-character hyphenated string is bulky to store and index, and — the one that bites people — randomness assumes the random source is trustworthy, which is covered below. What v4 gives you is offline, coordination-free generation: any node, any shard, any client can mint an ID that will not clash with anyone else's, which is why it became the default for distributed systems.

The problem v4 causes databases

Most storage engines keep primary-key indexes as B+trees, which store rows in key order. That ordering is a gift — if your keys arrive roughly in order. New rows append to the right edge of the tree, pages fill up sequentially and stay compact. Now scatter 122 random bits across that tree: every insert lands at a random position, which means a random page from disk, which means pages that were 90% full suddenly split into two half-full pages to make room. Multiply by a billion rows.

The symptoms are well known even when the cause is not: the clustered index grows far beyond the size of the data (poor page fill), writes become random I/O instead of sequential, cache hit rates collapse, and range scans slow down. Secondary indexes inherit the pain because each of their entries stores the primary key alongside itself. Random UUIDs are not "slow" in the abstract — they are slow for exactly the index structure that most databases use for primary keys.

v7: put the clock first

RFC 9562, published in 2023, standardized version 7. The layout is the same 128 bits with a different assignment: the first 48 bits hold a Unix timestamp in milliseconds, followed by random bits and the usual version and variant nibbles. Because the timestamp leads, v7 UUIDs sort lexicographically and numerically by creation time. Generate one per millisecond and the remaining random bits keep ties within the same millisecond vanishingly unlikely.

For a database this is the whole point: inserts now arrive in time order, so they append to the right edge of the B+tree instead of stabbing it at random. Pages fill sequentially, splits become rare, the index stays compact, and you keep every property that made v4 attractive — global uniqueness, offline generation, no coordination, no enumeration of your record count.

The trade-off is disclosure: anyone who sees a v7 UUID knows roughly when the record was created, to the millisecond. For internal row identifiers that is harmless and useful. For identifiers attached to private events — a user's signup, a medical record, a session — it leaks a timeline to anyone holding the ID. That is the honest cost of sortedness, and it is also the answer to "should I just switch everything to v7": default to yes for primary keys, think twice for user-visible identifiers.

The other versions, briefly

  • v1 — time plus a MAC address. Sortable but leaks hardware identity; superseded by v7.
  • v6 — v1's timestamp reordered for better sorting; a compatibility artifact.
  • v3 / v5 — derived from a name plus a namespace via MD5 / SHA-1. Deterministic, so the same input always yields the same ID; great for stable imports, terrible for anything secret.
  • v8 — reserved for custom layouts; use it if you need a UUID-shaped ID with your own bits inside.

When a UUID is the wrong choice

A plain auto-increment integer is smaller, faster to index, and free to generate if your writes go through one database. Use it when IDs do not need to be unguessable and you can tolerate leaking your row count — or hide the count with a separate public ID while keeping the integer internal. Its weaknesses are real but specific: clients cannot generate IDs offline, multi-master and sharded setups need sequence coordination, and enumerable IDs demand authorization checks on every access.

If you want v7's properties in a shorter string, ULID is the alternative: also 128 bits, but encoded as 26 characters of Crockford Base32 instead of 36 characters of hex, with a 48-bit millisecond timestamp up front and optional monotonic ordering within the same millisecond. The same time-then-random logic appears in Snowflake-style IDs at large scale, and even MySQL's UUID_TO_BIN exists mainly to swizzle v1's scattered time fields to the front so its indexes behave.

The random source is not optional

Everything above assumes v4's 122 bits come from a cryptographically secure generator, and history is littered with libraries that quietly used the wrong one. Math.random() in JavaScript is a statistical PRNG: its outputs, a few of them, let a watcher reconstruct its internal state and predict every "random" value that follows. That has produced real incidents — predictable session IDs, reset tokens guessable from page source. Use crypto.randomUUID() in browsers and modern Node, crypto.getRandomValues() everywhere else, and the platform CSPRNG in any backend language; treat a fallback to a non-cryptographic generator as a bug report, not a warning.

The format itself is only as strong as the bits you put into it. You can generate v4 and v7 UUIDs, inspect the version and variant nibbles, and pick the random source explicitly right here, entirely in your browser.

UUID Generator — v4 & v7Random & time-ordered IDs — open the tool