Development

UUID, ULID and IDs: when to use each

"It is just a random id" is where many architecture decisions die early. A UUID v4 is great for not colliding, but poor as a primary key the database has to keep sorted. A ULID solves the ordering, but exposes the creation time. Choosing between UUID v1, v4, v7 and ULID is choosing what you accept leaking and what you must guarantee. This guide opens the anatomy of each format, bits, version, variant, walks the eight versions of RFC 9562, computes the real chance of collision, shows how to extract the date out of a sortable id and compares the market alternatives. Generate and validate yours in the [UUID generator](tool:gerador-uuid) and read the timestamp of a sortable id in the [timestamp converter](tool:conversor-timestamp).

J-Kit17 min readIntermediate
  • UUID
  • ULID
  • Database
  • Primary key
  • Development

Key takeaways

  • A UUID is 128 bits; v4 spends 122 of them on randomness, the rest mark version and variant. RFC 9562 defines eight versions, v1 to v8.
  • v7 and ULID are time-sortable, which gives index locality; v4 is random and triggers page splits that fragment the B-tree.
  • Sortable IDs leak the creation time; v4 is opaque. Choose by what you accept exposing.
  • Beyond UUID there are Snowflake, KSUID and NanoID; large numeric ids must travel as strings in JSON so they keep their precision.

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.

128 bitstotal size of any UUID
36characters in canonical form (with hyphens)
6 bitsfixed: 4 for version + 2 for variant
  1. 2005RFC 4122

    The first standardized UUID specification, with versions v1, v3, v4 and v5. None of them is usefully time-sortable for databases.

  2. 2016ULID

    Alizain Feerasta publishes the ULID spec: 128 bits with time up front and lexicographic ordering, solving what UUID still did not.

  3. 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.

Generate v4 UUIDs with Web Crypto or paste an id in the validate tab to read version and variant, all in the browser, no network.Open the tool full page

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.

The eight UUID versions of RFC 9562, each with its section.
VersionContentSectionSortable?
v1time (since 1582) + MAC§5.1Partial
v2DCE Security (rare)§5.2No
v3MD5 hash of namespace + name§5.3No
v4122 random bits§5.4No
v5SHA-1 hash of namespace + name§5.5No
v6v1 with the time reordered§5.6Yes
v748-bit Unix timestamp + random§5.7Yes
v8free / experimental (you define the bits)§5.8Depends

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 / randomness
Example ULID from the spec: time on the left, chance on the right.

One 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
Approximate number of ids for a 50% chance of one collision. For b = 122: sqrt(2^122) = 2^61 ≈ 2.3 × 10^18; multiplied by 1.177, it gives n ≈ 2.7 × 10^18.
5.3 × 10³⁶possible v4 values (2^122)
2.7 × 10¹⁸generated for a 50% chance of 1 collision
~86 yearsgenerating 1 billion per second

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.

  1. 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).
  2. 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.
  3. 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.
  4. 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)
Extracting the date: the ULID’s first 10 characters become 1469922850259 ms; the same instant in a v7 would be 01563e3a-b5d3 in the first 12 hex.

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.

The formats side by side: bits, ordering, opacity, textual size and typical use.
FormatBitsSortableOpaqueTextTypical use
UUID v4128 (122 rand.)NoYes36generic id, public token
UUID v7128YesNo36sortable primary key
ULID128YesNo26sortable, short key/URL
Snowflake64YesPartialup to 19 digitsdistributed id (Twitter, Discord)
KSUID160YesNo27sortable id with more entropy
NanoID~126 (default)NoYes21short id in a URL
bigint / bigserial64Yes (sequential)Noup to 19 digitslocal sequential PK
bigint8 B
UUID binary(16)16 B
NanoID (21)21 B
ULID (26)26 B
UUID text (36)36 B
Storage bytes per identifier. Text formats assume ASCII (1 byte per character); binary(16) and bigint are the raw size. Same unit across the whole chart: bytes.
View the data
CategoryValue
bigint8 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?
Mathematically yes, but the chance is negligible: with 122 random bits, it would take about 2.7 × 10^18 UUIDs for a 50% probability of a single collision. The practical risk only appears with weak generators; use a cryptographic source like Web Crypto.
What is UUID v7 and why is it recommended?
It is the RFC 9562 version (§5.7) that puts a 48-bit Unix millisecond timestamp in front and randomness behind. That makes it time-sortable, which gives index locality and improves database inserts, fixing v4’s "poor index locality" without leaving the standard UUID format.
Can I tell when a UUID was created?
It depends on the version. A v4 is pure chance and stores no time at all. But v1, v6, v7 and ULID carry the creation instant in their leading bits: extract the time field, decode it to milliseconds and convert to a date. That is why a sortable id is never anonymous about its timestamp.
Is ULID better than UUID?
Not in absolute terms, it depends on the goal. ULID wins when you need time ordering and a short, URL-safe string. UUID v4 wins when the id must not reveal the creation time. For a primary key, UUID v7 brings ULID’s ordering in the standard UUID format.
UUID or Snowflake: which should I use?
Snowflake fits in 64 bits and is sortable, great when id size matters and you control the generating nodes (like Twitter and Discord). UUID v7 and ULID are 128 bits and need no node coordination, any process generates one with no collision risk. If you do not need to squeeze the id into 8 bytes, v7 is usually simpler to operate.
Should I store a UUID as text or binary?
Binary, when you can. The textual form uses 36 bytes; the binary form, 16. Because the primary key repeats across indexes and foreign keys, the extra 20 bytes per row become a lot of index space. Use the native uuid type (PostgreSQL) or BINARY(16) (MySQL) and convert to text only at the API edge.
How many characters does each format have?
A canonical UUID has 36 characters (32 hex plus 4 hyphens) for 128 bits. A ULID has 26 characters in Crockford Base32 for the same 128 bits. KSUID has 27, NanoID 21 by default, and a Snowflake or bigint reaches up to 19 digits.

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

  1. RFC 9562, Universally Unique IDentifiers (UUIDs), May 2024 (obsoletes RFC 4122)
  2. ULID, Official specification (github.com/ulid/spec)
  3. MDN, Crypto.randomUUID() (generates v4 with a cryptographic RNG)
  4. Twitter Snowflake, original implementation (github.com/twitter-archive/snowflake)
  5. KSUID, K-Sortable Unique IDs (github.com/segmentio/ksuid)
  6. NanoID, id generator (github.com/ai/nanoid)
  7. PostgreSQL, uuid data type (16 bytes)