What a UUID is
A UUID — universally unique identifier, also called a GUID — is a 128-bit value written
as 32 hexadecimal digits in five hyphen-separated groups:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. Its defining property is that you can create one
without asking anyone. No central registry, no database sequence, no coordination between servers — two
machines that have never communicated can each mint identifiers and the values will not collide.
That independence is why UUIDs are everywhere in distributed systems. A mobile client can create a record offline and assign its ID immediately, then sync later without renumbering. A service can generate a correlation ID at the edge and thread it through every downstream call. A batch job can insert a million rows across shards without a shared counter becoming a bottleneck. The format is defined by RFC 9562, which replaced RFC 4122 in 2024 and added versions 6, 7 and 8 to the family.
The bit layout, and why 122 and not 128
Not all 128 bits are free. Four bits — the first hexadecimal digit of the third group — hold the
version, and the two or three most significant bits of the fourth group hold the
variant. That is why every standard UUID has one of 8, 9,
a or b at the start of its fourth group: those four hex digits are exactly the
values whose top two bits are 10. Six fixed bits leave 122 for randomness in a version 4
value. In version 7, 48 of the remaining bits are spent on a timestamp, leaving 74 random bits.
Both fields matter in practice. A value that is 32 hex digits long but has, say, a c
starting the fourth group is a legacy Microsoft GUID, not an RFC-variant UUID, and some strict parsers
will reject it. The validator above reports both fields so you can tell a truncated or hand-edited value
from a genuine one.
UUID v1 vs v4 vs v7 — and when to use each
| Version | Built from | Sortable | Leaks | Use it when |
|---|---|---|---|---|
| v1 | 100-ns timestamp + clock sequence + MAC address | Poorly | Creation time and the host's MAC address | Almost never in new code — legacy interop only |
| v3 | MD5 of a namespace UUID and a name | No | Nothing, but MD5 is broken | Interoperating with an existing v3 scheme |
| v4 | 122 random bits | No | Nothing | Public-facing identifiers, tokens, correlation IDs |
| v5 | SHA-1 of a namespace UUID and a name | No | Nothing | Deterministic IDs derived from a natural key |
| v6 | v1 fields reordered so time is most significant | Yes | Creation time, optionally the MAC | Migrating an existing v1 estate to a sortable form |
| v7 | 48-bit Unix ms timestamp + 74 random bits | Yes | Creation time, to the millisecond | Database primary keys, event IDs, anything time-ordered |
| v8 | Whatever you define, within the version/variant frame | Your choice | Your choice | You genuinely need a custom layout and know why |
Version 1: a timestamp and a network card
Version 1 was the original design. It combines a 60-bit timestamp counting 100-nanosecond intervals since 15 October 1582, a clock sequence to survive clock rollbacks, and the 48-bit MAC address of the generating machine. Uniqueness is guaranteed by construction rather than by probability: no two machines share a MAC address, and no machine generates two values in the same 100-nanosecond tick.
The problems are twofold. First, it publishes the MAC address of the host that created it, which is an information leak — this is the property that famously helped identify the author of the Melissa virus in 1999. Second, the timestamp is stored with its low bits first, so consecutive v1 values do not sort chronologically as strings even though they encode time. Version 6 exists purely to fix that byte ordering. In new systems, reach for v7 instead of either.
Version 4: randomness, and nothing else
Version 4 UUIDs are almost entirely random: 122 of the 128 bits come from a random source. They carry no
information — no timestamp, no machine address, nothing about the record they identify — which makes them
both privacy-preserving and unguessable. If an identifier will appear in a URL, an email, a webhook
payload or a support ticket, that opacity is a real feature. An attacker who sees
/invoices/3f2a9c1e-… learns nothing about how many invoices you have or when this one was
created.
The generator on this page uses the Web Crypto API rather than Math.random. That distinction
matters: Math.random is a fast pseudo-random generator whose internal state can be recovered
from a handful of outputs, which would make every subsequent identifier predictable. Cryptographic
randomness closes that gap. Any library that builds UUIDs from Math.random — and several
popular ones historically did — produces values that look fine and are not safe to expose.
Version 7: the same guarantees, in order
Version 7 is the newest general-purpose version and is increasingly the recommended default for anything
stored in a database. Its layout is deliberately simple: the first 48 bits are the Unix timestamp in
milliseconds, big-endian, so they read left to right in the canonical string. Then 4 bits of version
(0111, which is why v7 values show a 7 where a v4 shows a 4), 12
bits that may be random or, as here, a monotonic counter. Then 2 bits of variant (10), and
62 more random bits.
The consequence is that string comparison, byte comparison and creation order all agree. Sort a list of v7 UUIDs alphabetically and you have sorted them chronologically. That single property is what makes them so much better behaved as database keys, and it also means you can read a rough creation time out of an ID during an incident without a database round trip. Generate a batch above with v7 selected and you can see the shared leading digits drift as the milliseconds tick over.
The trade-off is that v7 does reveal creation time. For an internal primary key that is usually harmless or even useful. For a public identifier where the creation time is sensitive — a medical record, a job application, anything where "account number 7 was created three seconds after account number 6" tells a competitor something — v4 remains the right answer. Plenty of schemas use both: a v7 key internally and a v4 exposed externally.
Why random UUID keys fragment a B-tree index
This is the single most common reason teams regret choosing UUIDs, and it is worth understanding precisely rather than as folklore.
A relational index is a B-tree: a sorted structure made of fixed-size pages, typically 8 or 16 KB. When you insert a row, the engine finds the leaf page where the new key belongs and writes it there. With a monotonically increasing key — an auto-increment integer, or a v7 UUID — every insert belongs at the right-hand edge of the tree. That one page stays in memory, fills up, splits once cleanly, and the next page becomes the new hot page. Written pages are full, the working set is tiny, and the cache hit rate is close to perfect no matter how large the table grows.
With a random v4 key, every insert lands in a uniformly random leaf. Three things go wrong at once. Page splits multiply: an insert into the middle of a full page forces the engine to split it into two half-full pages, so the index physically grows faster than the data and typical fill factors drift toward 50–70%. The working set becomes the whole index: since any insert may touch any page, the engine must keep the entire index hot to avoid disk reads, and once the index exceeds the buffer pool you start paying a random read per insert. Write amplification climbs: every touched page must be flushed and, in a write-ahead-log system, logged — MySQL InnoDB will even log a full page image the first time a page is modified after a checkpoint.
InnoDB feels this most sharply because it is a clustered index: the primary key is the physical row order, and every secondary index stores the primary key as its row pointer. A random 16-byte primary key therefore fragments the table itself and inflates every secondary index. PostgreSQL's heap storage decouples row placement from the index, so the table stays append-ordered, but the B-tree index over a random UUID still suffers the split-and-cache problems, and Postgres' opportunistic HOT updates and index deduplication both work less well against random keys.
Version 7 fixes all of this without giving up decentralised generation. Because the high bits are a timestamp, inserts arrive in near-sorted order and behave almost exactly like an auto-increment key: the rightmost page stays hot, splits are clean, and index size stabilises. "Near" rather than "exactly" because several machines generating concurrently will interleave within each millisecond, but that disorder is bounded to one page's worth of keys rather than the whole tree. Teams that migrate a hot table from v4 to v7 keys routinely report insert throughput improving by a large multiple and index size falling by a third or more.
If you are stuck with v4 keys for compatibility reasons, the usual escape hatches are to keep an internal auto-increment or v7 clustered key and carry the v4 as a unique secondary column that you expose externally, or — on MySQL — to store a byte-swapped form that moves the more time-correlated bytes to the front. The first option is simpler and works everywhere.
Collision probability, in plain terms
"Universally unique" is a probabilistic claim, not a mathematical guarantee, so it is fair to ask how strong the claim actually is. A version 4 UUID has 122 random bits, which is about 5.3 × 1036 possible values. Collisions follow the birthday problem: you reach a 50% chance of one collision after roughly the square root of that space, which is about 2.7 × 1018 — 2.7 quintillion UUIDs.
Some ways to feel that number:
- Generating a billion UUIDs per second, you would pass the 50% mark after about 85 years.
- After generating 100 trillion UUIDs — more than one for every cell in a hundred thousand human bodies — the probability of any collision is still under one in a billion.
- For a system that mints a million IDs a day for a century, the chance of ever seeing a duplicate is somewhere around 10-16. You are many orders of magnitude more likely to lose the row to undetected memory corruption or a cosmic-ray bit flip.
Version 7 is different in structure but at least as safe in practice. Two v7 values can only collide if they were generated in the same millisecond, and within a millisecond you have 74 bits — a collision needs roughly 237 values, about 137 billion, in that one millisecond. The implementation here goes further and uses the 12-bit field after the version as a counter that increments within a millisecond, so values from this page cannot collide with each other at all until the counter wraps at 4096 per millisecond.
The real-world risk is never the mathematics. It is a broken generator: a library using
Math.random, a container fleet whose instances all seed from the same value at boot, an
embedded device with no entropy source, or code that "generates" a UUID once and caches it. If you ever
do see duplicate UUIDs, look at the generator, not the odds.
UUID vs auto-increment vs ULID vs NanoID
| Scheme | Size | Text length | Sortable | Needs coordination | Reveals |
|---|---|---|---|---|---|
| Auto-increment integer | 4 or 8 bytes | 1–19 chars | Yes | Yes — one writer or a sequence server | Row count and creation order |
| UUID v4 | 16 bytes | 36 chars | No | No | Nothing |
| UUID v7 | 16 bytes | 36 chars | Yes | No | Creation time (ms) |
| ULID | 16 bytes | 26 chars (base32) | Yes | No | Creation time (ms) |
| NanoID | ~15 bytes default | 21 chars (base64url) | No | No | Nothing |
| Snowflake ID | 8 bytes | ≤19 chars | Yes | Yes — unique worker IDs | Creation time and worker ID |
Auto-increment is still the cheapest option in every dimension that a database cares
about: 8 bytes instead of 16, perfect insert locality, and integer comparisons. Its costs are
organisational rather than technical. You cannot know an ID before the insert, which makes client-side
creation and offline-first clients awkward; merging data from two environments means renumbering; and the
IDs are enumerable, so exposing them publicly tells the world your growth rate and invites people to walk
/users/1 through /users/n. That last problem is an access-control bug either
way — see 403 Forbidden for how a properly guarded endpoint should
respond — but sequential IDs make it trivially exploitable.
ULID and UUID v7 are close cousins: both are 128 bits with a 48-bit
millisecond timestamp in front. ULID came first and its selling point is the 26-character Crockford
base32 text encoding, which is shorter, case-insensitive and avoids the letters I, L, O and U to reduce
transcription errors. UUID v7's selling point is that it is a ratified IETF standard that drops straight
into every existing uuid column, ORM mapping, log parser and validation regex you already
have. If your stack already speaks UUID, v7 is the lower-friction choice; ULID is attractive mainly when
the identifier is meant to be read aloud or typed by a human.
NanoID optimises for URL length rather than for databases. Its default 21-character base64url alphabet gives about 126 bits of entropy in 21 characters instead of 36, which is a real win for short links and public slugs, but it has no standard binary form, so databases store it as text and you lose the 16-byte representation entirely. Snowflake IDs — Twitter's design, also used by Discord — squeeze a timestamp, worker ID and per-worker sequence into 64 bits. They are compact and sortable, but every generator must be assigned a unique worker ID, which reintroduces exactly the coordination that UUIDs were invented to avoid.
A reasonable default for most new systems: UUID v7 as the primary key, stored in a native 16-byte column, and either the same value or a separate v4 exposed in public URLs depending on whether creation time is sensitive.
Storage: 36 characters or 16 bytes
A UUID is 128 bits. That is 16 bytes. The canonical text form is 36 characters — 32 hex digits plus 4
hyphens — and if you store it as VARCHAR(36) you have made every identifier in your database
more than twice as large as it needs to be, before per-row string overhead.
The cost compounds through the indexes. In MySQL InnoDB the primary key is stored again inside every
secondary index, so a table with a CHAR(36) primary key and four secondary indexes carries
five copies of that 36-byte string per row instead of five copies of 16 bytes. Index pages hold fewer
entries, so the tree is deeper and each lookup touches more pages, and the buffer pool caches
proportionally less of it. On a hundred-million-row table this is the difference between an index that
fits in RAM and one that does not.
What to use where:
- PostgreSQL: the native
uuidtype, which is exactly 16 bytes and has its own comparison operators. Nevertext. - MySQL / MariaDB:
BINARY(16), withUUID_TO_BIN()andBIN_TO_UUID()at the boundary. MySQL 8'sUUID_TO_BIN(uuid, 1)also swaps the time fields of a v1 UUID for better locality — irrelevant and unnecessary for v7, which is already ordered. - SQL Server:
uniqueidentifier, 16 bytes. Be aware its sort order for this type is famously non-obvious, comparing the last group first. - SQLite: a
BLOBof 16 bytes, since there is no native type. - MongoDB: BSON
UUIDsubtype 4 rather than a string. - JSON APIs: always the canonical lowercase 36-character string. Binary is a storage detail; your wire format should stay readable. Use the JSON formatter to check a payload full of them.
One caveat worth stating plainly: if your ORM or query layer is going to make binary storage painful enough that people work around it, a 36-character column on a table of modest size is not a crisis. Make the binary choice where the row counts justify it, and be consistent about it.
Where UUIDs earn their keep in API work
- Idempotency keys. Send a fresh UUID in an
Idempotency-Keyrequest header on a payment or order request. If the network drops and the client retries, the server recognises the key and returns the original result rather than charging twice. See the HTTP methods guide for which verbs are idempotent by definition and which need this help. - Correlation and trace IDs. One UUID per inbound request, logged by every service it touches, turns a distributed failure into a single greppable string. v7 is especially good here because the ID itself tells you roughly when the request started.
- Client-generated resource IDs. Letting the client mint the ID makes
PUTnaturally idempotent — retrying creates nothing new because the identity was fixed before the request left. A duplicate create can then answer 409 Conflict instead of silently making a second row. - Test fixtures. Unique values per test run avoid the collisions that come from hardcoded IDs in a shared environment. Generate a batch here and paste them into the request body on the API tester, or work through the REST API testing guide for a fuller workflow.
- Opaque public identifiers. Exposing
/orders/8f14e45f…instead of/orders/1042stops anyone counting your customers or walking through records by incrementing the number. - Token identifiers. The
jticlaim in a JSON Web Token is conventionally a UUID, and it is what a denylist keys on when you need to revoke a single token — paste one into the JWT decoder to see it in context.
Common pitfalls
Treating a UUID as a secret
Unguessable is not the same as confidential. Identifiers in URLs are captured in server logs, proxy logs,
browser history, bookmarks and Referer headers. A "secret" share link backed only by a UUID
leaks the moment someone pastes it into a chat that unfurls previews. Real access control belongs behind
authentication — see 403 Forbidden for how to signal a denied
request properly, and 401 Unauthorized for a missing or invalid
credential.
Case and format inconsistency
UUIDs are case-insensitive by specification, and the canonical form is lowercase — but string comparison
is not case-insensitive. A system that stores A1B2… and looks up a1b2… will find
nothing. Normalise to lowercase at the boundary and be equally consistent about whether hyphens are kept.
The same applies to the braced {…} form that .NET's Guid.ToString("B") produces
and the urn:uuid: prefix that some standards use.
Assuming any 36-character string is a UUID
The version and variant nibbles are part of the format. A validation regex of
[0-9a-f]{8}-[0-9a-f]{4}-… accepts values that no conforming generator would produce. The
validator above reports version and variant, which is a quick way to spot a value that was truncated,
hand-edited or generated by something that only approximates the spec.
Sorting v4 UUIDs and expecting meaning
ORDER BY id on a v4 column returns a stable but arbitrary order that has nothing to do with
insertion time. Pagination built on it is consistent, which is often all you need — but if you meant
"newest first" you need a created_at column or a v7 key.
Generating them in a loop on the server for no reason
UUID generation is cheap but not free, and some libraries block on the system entropy pool. Generating hundreds of thousands per second in a tight loop on a container with a starved entropy source has caused real production stalls. Modern kernels and language runtimes make this rare, but it is worth knowing the failure mode exists.
Putting a UUID where a natural key belongs
A join table between two entities that already have keys does not need its own UUID. Neither does a lookup table of currency codes. Adding surrogate identifiers everywhere by reflex costs storage and index space for nothing.
Frequently asked questions
Are these UUIDs cryptographically random?
Yes. They come from crypto.getRandomValues, the browser's CSPRNG, not from
Math.random. A version 4 value has 122 bits of entropy; a version 7 value has 74 random bits
plus a 48-bit timestamp.
Could two of them collide?
Not in any practical sense. You would need to generate about a billion version 4 UUIDs per second for roughly eighty-five years before the odds of one collision reached fifty percent. Version 7 values can only collide within a single millisecond, and the counter used here prevents even that for the first 4096 values per millisecond.
What is the difference between UUID v4 and UUID v7?
Version 4 is pure randomness and carries no information, so successive values are unrelated and unsorted. Version 7 puts a 48-bit Unix millisecond timestamp in the leading bits, so values sort in creation order. Use v4 when the identifier is public and should reveal nothing; use v7 when it is a database key and insert locality matters.
Should UUIDs be my primary keys?
They can be, with care. Random v4 values scatter inserts across a clustered B-tree, causing page splits, index bloat and a working set the size of the whole index. UUID v7 gives you global uniqueness with sequential ordering and behaves almost like an auto-increment key, which is why it is the better default for new tables.
How should I store a UUID in the database?
As 16 bytes, not 36 characters. Use PostgreSQL's native uuid type, MySQL
BINARY(16) with UUID_TO_BIN(), SQL Server uniqueidentifier, or a
16-byte BLOB in SQLite. Keep the canonical 36-character string for JSON and URLs.
Is UUID v7 better than ULID?
They are near-identical in structure. UUID v7 is a ratified standard (RFC 9562) that fits existing UUID columns and tooling. ULID's advantage is its shorter 26-character base32 text form. If your stack already speaks UUID, v7 is the lower-friction choice.
What are the nil and max UUIDs for?
The nil UUID is all zero bits and is the specification's placeholder for an absent or unset identifier. The max UUID is all one bits and is occasionally used as an upper sentinel in range scans. Both are valid UUIDs but belong to no version.
Are the UUIDs generated on a server?
No. They are created in your browser and never transmitted, so no one else — including us — has ever seen them. Open the network tab while pressing Generate and you will see no requests at all.
Why do all version 4 UUIDs have a 4 in the same place?
That digit is the version field, and the first character of the fourth group encodes the variant — always 8, 9, a or b for standard UUIDs. Both are fixed by the specification, and together they consume the 6 bits that reduce a v4's entropy from 128 to 122.
Related tools and reference
API Tester
Use a generated UUID as an idempotency key and watch how the endpoint responds to a retry.
JSON Formatter
Build and validate a request body containing your new identifiers.
JWT Decoder
Inspect the jti claim — the UUID that uniquely identifies a token.
Base64 Encoder
Encode a 16-byte UUID for transport, or decode one you found in a header.
HTTP Headers Guide
Idempotency-Key, X-Request-ID and the other headers UUIDs travel in.
409 Conflict
The right response when a client-generated identifier already exists.