How UUIDs Work: Structure, Versions, and Collision Math
The anatomy of a UUID, what each version means, how randomness fills 128 bits, and why collisions are theoretical while validation still matters.
By Rehan Baqir
Related
A UUID is 128 bits with fixed positions for a version and a variant. The rest is a timestamp, random bits, or a hash of a name, depending on the version. Generate a sample with the UUID generator. It is an identifier, not a secret and not a lottery draw. Someone pasted 550e8400-e29b-41d4-a716-446655440000 into Slack and asked if it was random enough for lottery numbers. That string is the canonical example from RFC 4122, not a fresh secret. Treating the hex as mystery text is how the versions get mixed up.
This page is the layout. The version choice is UUID v4 versus v7. Regex Tester and JSON Formatter are the checks once an API starts sending these strings.
What a UUID actually is
A Universally Unique Identifier is a 128-bit value. In canonical form:
550e8400-e29b-41d4-a716-446655440000
|______|___|___|___|____________|
8 4 4 4 12 hex digits
That is 32 hexadecimal characters (0-9, a-f) in five groups separated by hyphens. Case does not matter for parsing. Lowercase is a common API convention.
128 bits means 2^128 possible values, roughly 340 undecillion combinations. You will not exhaust the space in a career.
Generate samples with UUID Generator. Each refresh is a different string.
The bit layout: version and variant
Not every bit is random. The standard reserves positions.
The version is 4 bits at a fixed position. It identifies how the UUID was generated (v1, v4, v7, and the others).
The variant bits indicate which UUID specification family this follows. The common RFC 4122 variant sets particular bits to 10.
In a UUID like e29b-41d4-a716, the third group's first digit (4 in 41d4) often hints at version 4. Parse it formally anyway. Eyeballing is how bad strings get stored.
Validators can reject garbage before it hits the database.
UUID versions you will actually see
Version 1: timestamp and MAC address
Version 1 combines a 60-bit timestamp (100-nanosecond intervals since the UUID epoch), a clock sequence for uniqueness inside the same timestamp, and a node ID. Historically the node ID was a MAC address. It is often randomized now, for privacy.
It is roughly time-ordered, and it does not need a central coordinator. A real MAC can leak hardware info. Clock rollback has edge cases.
Version 4: random
Version 4 sets the version bits to 0100 and fills the remaining bits from a CSPRNG in production. It is simple and opaque, and it does not leak hardware. Random order can fragment database B-tree indexes at very large scale. v4 uses 122 bits from the random generator once the version and variant bits are fixed.
This is the default most developers reach for. UUID in API development covers when to put them in REST paths and how to index them.
Version 7: time-ordered random
Version 7 puts a Unix timestamp in the leading bits and fills the rest with randomness. IDs are roughly sortable by creation time, which gives better index locality than v4. Library support is still catching up.
On a new Postgres app, v7 is worth comparing with v4.
Versions 3 and 5: name-based
Versions 3 and 5 build a deterministic UUID from a namespace UUID plus a name string, using MD5 (v3) or SHA-1 (v5). The same inputs always produce the same UUID.
Use them when you need a stable ID derived from content, such as a DNS name or a URL, and you do not want a mapping table. Skip them when you need unpredictability. If the namespace and the name are known, the UUID can be guessed.
Collision probability: the birthday problem
"Unique" here means the probability of a duplicate is negligible. It does not mean a duplicate is impossible.
For v4 UUIDs with decent randomness, a collision before the heat death of the universe is not the operational problem. Database unique constraints and application validation exist because bugs happen: a hardcoded test UUID copied to production, a client sending a malformed string, an integer ID cast into a UUID column.
Validate format with Regex Tester using a pattern like:
^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
Adjust the version nibble [1-5] if you only accept specific versions.
How generation works in practice
Language runtimes: crypto.randomUUID() in modern browsers and Node, the uuid npm package, uuid_generate_v4() in databases.
Database defaults: PostgreSQL gen_random_uuid(), MySQL UUID().
Manual tools: UUID Generator for fixtures, demos, and a quick copy.
Production needs a cryptographically secure random source. Math.random() is not enough for security-sensitive IDs. It is fine for throwaway test data.
Storage formats
| Format | Example | Notes |
|--------|---------|-------|
| Canonical string | 550e8400-e29b-41d4-a716-446655440000 | API JSON, logs |
| Binary(16) | 16 raw bytes | Compact DB storage |
| URN | urn:uuid:550e8400-... | Some XML systems |
Store a native uuid type when the database has one. VARCHAR works, wastes space, and skips format checks. A 16-byte UUID is twice the width of a BIGINT. Stored by mistake as VARCHAR, it is 36 bytes, which grows indexes and the memory used by foreign keys.
UUIDs and other ID schemes
| Scheme | Sortable | Size | Predictable | |--------|----------|------|-------------| | Auto-increment int | Yes | 4-8 bytes | Yes | | UUID v4 | No | 16 bytes | No | | UUID v7 | Roughly | 16 bytes | No | | ULID | Yes | 16 bytes | No | | Snowflake | Yes | 8 bytes | Partially |
UUIDs fit distributed generation with no coordinator. Integers fit compact sequential URLs when you do not publish them.
Common misconceptions
UUIDs are unique in the probabilistic sense. Treat them as unique, and enforce that with a database constraint.
A shorter UUID breaks interoperability. If length matters, use a ULID or a custom short code.
Uppercase and lowercase parse the same. Pick one convention for display.
A UUID in a URL is not an authorization check. Confirm ownership on the server. Knowing the identifier must not bypass that check. Debugging a 36-character hex string in a log or a support call is harder than reading a short prefixed ID.
Serialization in JSON and APIs
JSON has no binary UUID type. APIs send UUIDs as strings:
{
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"customer_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
Inspect responses with JSON Formatter. Consistent hyphenation keeps client cache keys from splitting.
Encoding binary UUID bytes for transport is a different operation. See Understanding Base64 Encoding.
Testing with UUIDs
Generate a unique fixture per test case. Do not reuse one shared constant. Test malformed input: wrong length, wrong version nibble, non-hex characters. Test case insensitivity in parsers. Check that foreign keys survive a UUID string round trip.
Limitations
A UUID is larger than a 64-bit integer. A 16-byte value is twice a BIGINT, and 36 bytes if it is stored as VARCHAR, which increases index size and foreign-key memory.
It is not a security token. Knowing a resource's UUID must not bypass server-side authorization.
Raw 36-character hex is harder to read in logs and support calls than a short prefixed ID.
Related articles
- Using UUIDs in API Development covers API design, indexing, and security patterns
- Understanding Base64 Encoding separates encoding from identifier generation
- Regex Debugging Tips covers validating UUID format patterns
Version bits say how the UUID was made. Variant bits say which spec applies. The remaining bits carry a timestamp, randomness, or a hashed name. v4 is what most systems use. v7 is the alternative when sort order matters. A collision is unlikely to be the incident. Bad validation and a missing auth check are.
Frequently Asked Questions
Common questions answered to help you get the most from this tool.