Anatomy of a UUID
A UUID (Universally Unique Identifier) is 128 bits written as 32 hexadecimal digits in five groups: 8-4-4-4-12, totaling 36 characters with the hyphens. Within those bits, six are not free: four mark the version and two mark the variant (the standard’s scheme). In practice, the first digit of the third group marks the version, and the high bits of the fourth group mark the variant. Take 550e8400-e29b-41d4-a716-446655440000: the "4" before "1d4" says it is version 4, and the "a" opening "a716" carries the RFC 4122 variant bits, exactly what the UUID generator’s validate tab reports when you paste that value.
- 2005RFC 4122
The first standardized UUID specification, with versions v1, v3, v4 and v5. None of them is usefully time-sortable for databases.
- 2016ULID
Alizain Feerasta publishes the ULID spec: 128 bits with time up front and lexicographic ordering, solving what UUID still did not.
- May 2024RFC 9562
Obsoletes RFC 4122 and adds v6, v7 and v8, bringing time ordering into the UUID standard itself.
Because version and variant sit in fixed positions, you can recognize a UUID by regex, the ready-made pattern in the regex builder’s library requires exactly the version digit and the variant bits. If you want to understand that expression symbol by symbol, see the regex from scratch guide. And when you want to inspect a real id, use the generator right below: paste any UUID and it shows the version and variant those six bits declare.
The eight versions, v1 to v8
RFC 9562 defines eight versions; some you use every week, others almost never. v1 combines the time (in 100 ns intervals since 1582) with the machine’s MAC address, it is time-sortable but leaks the hardware. v4 is almost entirely random: 122 bits of chance, no time and no machine, which makes it opaque and unpredictable. v7, the newest, puts a 48-bit Unix millisecond timestamp at the front and fills the rest with randomness, uniting the best of both worlds: time order without revealing the MAC. Each version has its own section in the RFC, which makes checking the primary source easy.
| Version | Content | Section | Sortable? |
|---|---|---|---|
| v1 | time (since 1582) + MAC | §5.1 | Partial |
| v2 | DCE Security (rare) | §5.2 | No |
| v3 | MD5 hash of namespace + name | §5.3 | No |
| v4 | 122 random bits | §5.4 | No |
| v5 | SHA-1 hash of namespace + name | §5.5 | No |
| v6 | v1 with the time reordered | §5.6 | Yes |
| v7 | 48-bit Unix timestamp + random | §5.7 | Yes |
| v8 | free / experimental (you define the bits) | §5.8 | Depends |
Versions v3 and v5 are unlike all the others: they draw nothing at random. You supply a namespace (a UUID standing for a domain, like DNS or URL) and a name (any string), and the version computes a hash, MD5 in v3, SHA-1 in v5, of that combination. The result is deterministic: the same namespace with the same name always produces the same UUID. RFC 9562 (§6.5) is explicit: "UUIDs generated at different times from the same name in the same namespace MUST be equal." That is gold for idempotency, you derive the id from a stable piece of data (the user’s email, a resource URL) and get the same id back without querying the database. If deriving a fixed-size value from a string still feels fuzzy, the hashing vs. encryption vs. encoding guide nails the concept, and you can watch a SHA-1 come to life in the hash generator.
The three additions of RFC 9562, v6, v7 and v8, exist for a stated reason: v4 has, in the RFC’s own words, "poor database-index locality." v6 is for people already on v1 who want ordering: it takes the same v1 fields and reorders the time bits so that sorting by bytes becomes sorting by date. v7 is the recommendation for new systems: Unix millisecond timestamp in front, randomness behind. v8 is the wildcard, the standard reserves the version for experimental or vendor-specific layouts, where you control every bit except version and variant. It was v8 that gave the ecosystem an official place to fit formats like ULID inside the UUID envelope.
ULID: sortable and compact
ULID (Universally Unique Lexicographically Sortable Identifier) is also 128 bits, but written in 26 characters using Crockford’s Base32, a 32-symbol alphabet (0123456789ABCDEFGHJKMNPQRSTVWXYZ) that drops the letters I, L, O and U to avoid visual confusion and is URL-safe. The first 48 bits are a timestamp in milliseconds since 1970; the remaining 80 bits are random. Because time comes first and Base32 preserves order, sorting ULIDs as text sorts them by creation date, with no extra index.
UUID v4
- 36 characters, hex with hyphens.
- 122 random bits; no ordering.
- Reveals nothing about when it was created.
ULID
- 26 characters, Crockford Base32, no hyphens.
- 48 time bits + 80 random; sortable.
- Exposes the creation time in the prefix.
01ARZ3NDEKTSV4RRFFQ69G5FAV
└──────────┘└───────────────┘
48 bits 80 bits
timestamp aleatoriedade / randomnessOne fine detail separates a well-built ULID from an imitation: monotonicity. If two ULIDs are born in the same millisecond, the 48 time bits are identical, and drawing the 80 random bits independently could flip the order within that millisecond. The spec fixes this by rule: when the same millisecond is detected, the generator does not re-randomize, it increments the random component by 1 in the least significant bit, with carry. That way two ULIDs from the same millisecond sit one unit apart and stay strictly ordered. The limit is generating more than 2^80 ids in a single millisecond, when the increment overflows and generation fails, a scenario no real system reaches.
The real collision chance of v4
The question that always comes up: can two random v4 collide? In theory yes, but the numbers make the worry irrelevant. With 122 bits of randomness, there are 2^122 ≈ 5.3 × 10^36 possible values. By the birthday paradox, a 50% chance of a single collision only appears around the square root of the total space, and it is that square root the formula below computes.
n ≈ 1.177 × sqrt(2^b)- n
- number of ids generated for a 50% chance of one collision
- b
- bits of randomness in the format (122 in UUID v4)
- 1.177
- the constant √(2 · ln 2), from the birthday paradox
Let us do the math with real numbers. For v4, b = 122, so the square root of 2^122 is 2^61 ≈ 2.3 × 10^18. Multiplying by the constant 1.177 gives n ≈ 2.7 × 10^18, the 2.7 quintillion that shows up everywhere. Now translate it into time: generating 1 billion (10^9) UUIDs per second, that is 2.7 × 10^18 ÷ 10^9 = 2.7 × 10^9 seconds, or about 86 uninterrupted years, to reach a single one-in-two chance of a repeated pair. In practice, v4 collision is not an engineering concern, as long as the randomness source is cryptographic, like the Web Crypto used by the generator. The real risk lives in weak generators (Math.random) or predictable seeds, not in the math.
When to use each and how to read the date
The choice comes down to three axes: index locality, the need for ordering and what you accept exposing. Random IDs like v4 land in scattered positions of a B-tree index; each insert touches different pages, which fragments the index and hurts caching. Time-first IDs (v7, ULID) always arrive "at the end", keeping inserts contiguous, the very index locality that RFC 9562 cites as motivation for v7.
- Primary key in a high-volume database: prefer v7 or ULID for index locality.
- Public token, session id or anything that must not reveal when it was created: use v4, opaque by nature.
- Need to sort by creation with no date column: ULID solves it with lexicographic sorting.
- Need a deterministic id from a stable piece of data (idempotency): use name-based v5.
- Never expose the database sequential id in a public URL, it leaks the record count and enables enumeration.
A practical bonus of sortable ids: the leading 48 bits are the creation instant, so you read when the record was born without querying the database. The steps below work for both ULID and UUID v7, and the timestamp converter does the last step for you. If the time looks "wrong", it is probably the time zone: the value is always UTC, the time zones and daylight saving guide explains why the same instant changes its label from zone to zone.
- Isolate the time fieldIn a ULID, it is the first 10 characters; in a UUID v7, the first 12 hexadecimal digits (the first two groups, minus the hyphen).
- Decode to an integerULID: convert from Crockford Base32. v7: read the 12 hex as a number. The result is the time in milliseconds since 1970.
- Convert to a datePaste the milliseconds into the timestamp converter; it detects that values above 10^12 are in milliseconds and returns the date in UTC and local time.
- Confirm the versionA v4 has no embedded time, if you try this on a v4, you read noise, not a date.
ULID 01ARZ3NDEK TSV4RRFFQ69G5FAV
│ │
10 chars (48 bits) 16 chars (80 bits)
Base32 Crockford: 0 1 A R Z 3 N D E K
valor: 0 1 10 24 31 3 21 13 14 19
ms: 1469922850259
data: 2016-07-30T23:54:10.259Z (UTC)
v7 01563e3a-b5d3-7... (mesmos 48 bits em hexadecimal)Worked example: take the spec’s example ULID, 01ARZ3NDEKTSV4RRFFQ69G5FAV. The first 10 characters, 01ARZ3NDEK, are the timestamp. Decoding from Crockford Base32, each character becomes a value (0, 1, 10, 24, 31, 3, 21, 13, 14, 19) and together they form the integer 1,469,922,850,259, which is the instant in milliseconds since 1970. Paste that number into the timestamp converter and read the date: 30 July 2016, 23:54:10 UTC. When those ids travel inside a payload, treat them as strings so you do not lose precision; the JSON and YAML conversion guide explains why very large integers corrupt in JavaScript.
Alternatives and pitfalls
UUID and ULID are not the only options. Snowflake, created at Twitter, packs a sortable id into just 64 bits: 1 sign bit (always 0), 41 bits of millisecond timestamp from a custom epoch (about 69 years of range), 10 bits of machine identification (5 for datacenter + 5 for worker, up to 1,024 nodes) and 12 bits of sequence (4,096 ids per millisecond per node). That is why Twitter and Discord ids are long, sortable numbers. KSUID, from Segment, goes the opposite way on size: 160 bits (second-precision timestamp + 128 bits of chance), 27 characters in Base62, for those who want even more entropy. And NanoID swaps the UUID format for a short, configurable string, 21 characters by default, from a 64-symbol URL-safe alphabet, which gives 126 bits of randomness, collision comparable to v4 in a much smaller format.
| Format | Bits | Sortable | Opaque | Text | Typical use |
|---|---|---|---|---|---|
| UUID v4 | 128 (122 rand.) | No | Yes | 36 | generic id, public token |
| UUID v7 | 128 | Yes | No | 36 | sortable primary key |
| ULID | 128 | Yes | No | 26 | sortable, short key/URL |
| Snowflake | 64 | Yes | Partial | up to 19 digits | distributed id (Twitter, Discord) |
| KSUID | 160 | Yes | No | 27 | sortable id with more entropy |
| NanoID | ~126 (default) | No | Yes | 21 | short id in a URL |
| bigint / bigserial | 64 | Yes (sequential) | No | up to 19 digits | local sequential PK |
View the data
| Category | Value |
|---|---|
| bigint | 8 B |
| UUID binary(16) | 16 B |
| NanoID (21) | 21 B |
| ULID (26) | 26 B |
| UUID text (36) | 36 B |
The chart reveals the first silent pitfall: storing a UUID as text costs 36 bytes, more than double the 16 bytes of the binary form. That does not sound like much until you remember the primary key repeats in every secondary index and every foreign key, the waste multiplies. The two most common pitfalls, text storage and numeric precision loss, are detailed below, alongside the mechanism by which v4 fragments the index.
Page splits and write amplification
Mechanism: a B-tree index keeps its keys in ordered pages. When you insert a value that lands in the middle of an already-full page, the database has to split that page in two and copy half the rows to the new one, that is the page split. A sequential or time-first id (v7, ULID) always enters the rightmost page, so splits are rare and happen only at the tail. A random v4 lands anywhere, causing splits scattered across the whole tree.
Effect: each random insert dirties a different page, so a single commit ends up rewriting far more pages than the rows you inserted, that is write amplification. And because the hot pages are scattered, the buffer pool (the in-memory page cache) cannot hold the working set, and the database fetches more pages from disk. With a sortable id, the hot set is just the index tail, which fits in cache. RFC 9562 sums this up by saying v4 has "poor index locality"; there is no universal percentage because the impact depends on the engine, the page size and the volume, but the mechanism is always this.
v5 and idempotency: the deterministic id
Mechanism: v3 and v5 derive the id from a namespace plus a name via a hash (MD5 in v3, SHA-1 in v5). Because the hash is deterministic, the same input pair always yields the same UUID. That lets you compute a resource’s id from a stable piece of data, for example, the v5 UUID of "https://example.com/order/42" in the URL namespace, with no lookup table.
Use: idempotency. If a client resends the same request, you recompute the same id and catch the duplicate without a prior database lookup. MD5 and SHA-1 are weak for security, but there is no secret at play here, just stable derivation from a label, so they work fine. The difference between hashing, encryption and encoding is in the hashing vs. encryption vs. encoding guide.
UUID as text vs. binary(16)
Storing a UUID in a text column (CHAR(36) or VARCHAR) uses 36 bytes per value; storing it in the 16-byte binary form uses less than half. PostgreSQL has a native 16-byte uuid type; in MySQL, the classic pattern is BINARY(16). On a large table, with the id repeated across secondary indexes and foreign keys, that 20-byte-per-row difference becomes gigabytes of extra index and more pages to fit in cache. If you need the textual form in the API, convert at the edge, store as binary, display as text.
Frequently asked questions
Can a UUID v4 repeat?
What is UUID v7 and why is it recommended?
Can I tell when a UUID was created?
Is ULID better than UUID?
UUID or Snowflake: which should I use?
Should I store a UUID as text or binary?
How many characters does each format have?
Every UUID is 128 bits; the version decides what goes inside, and RFC 9562 spans v1 to v8. Use v4 when the id must be opaque and unpredictable, v7 or ULID when it must be sortable and index-friendly, and v5 when it must be deterministic. v4 collision is not a problem with a cryptographic source, what matters is choosing whether to expose the creation time, storing as binary to save index space, and treating large numeric ids as strings in JSON.
Sources & references
- RFC 9562, Universally Unique IDentifiers (UUIDs), May 2024 (obsoletes RFC 4122)
- ULID, Official specification (github.com/ulid/spec)
- MDN, Crypto.randomUUID() (generates v4 with a cryptographic RNG)
- Twitter Snowflake, original implementation (github.com/twitter-archive/snowflake)
- KSUID, K-Sortable Unique IDs (github.com/segmentio/ksuid)
- NanoID, id generator (github.com/ai/nanoid)
- PostgreSQL, uuid data type (16 bytes)