UUID and ULID Generator
Generate UUID v4, UUID v7 and ULID identifiers using the browser crypto API. Explains why v7 and ULID index better in a database than v4.
v4 carries 122 bits of randomness and no ordering at all. Used as a database primary key it scatters index inserts across the whole tree.
- 92a2a6e9-2695-48f9-b8d4-355898bf80aa
- 889478be-a0dc-4ab9-84e5-add4833f96cb
- 1529cc30-48ad-4ec9-a207-096156106af7
- 0d23a220-776f-4b47-999f-002812e99685
- 000123b1-6226-4ffc-86ee-1666aa5520f1
Inspect an identifier
Paste a UUID to see its version and, where it carries one, its embedded time.
Key Takeaways
- UUID v4 is fully random, so consecutive inserts land all over a B-tree index and fragment it.
- UUID v7 puts a 48-bit millisecond timestamp in the high bits, so it sorts by creation time and new rows append at the end of the index.
- ULID is the same idea in a 26-character Crockford base32 form that is URL-safe and case-insensitive.
- Both v7 and ULID LEAK creation time to anyone holding the id, which makes v4 the right choice when that timing is sensitive.
The primary key that shredded the index
You built the table with a UUID primary key and the first few months were uneventful. Past a million rows, inserts started getting slower and no single query could be blamed. The cause was not in the queries but in the key itself: because v4 is fully random, every new row lands on a random leaf of the index tree, forcing the database to fetch and split a different page each time instead of working at the hot end.
What actually separates v4 from v7
A v4 UUID carries 122 random bits, with the remaining 6 spent on the version and variant fields. That randomness makes an excellent identifier and a terrible index key: two v4 values generated a microsecond apart sort nowhere near each other. A v7 instead writes Unix milliseconds big-endian into the first 48 bits and leaves the rest random. The consequence is that lexicographic order matches creation order, new rows append at the end of the index, and the database keeps working the same warm pages.
# Consecutive v4 values (order is arbitrary)9f8a2c14-... 3b1e77d0-... c40d5e92-... 1a7f30bb-... # Consecutive v7 values (order is creation order)01994e2a-7c10-7... 01994e2a-7c10-7... 01994e2a-7c11-7...^^^^^^^^^^^^^ shared time prefix, increasing as the clock advances # ULID (26 characters, Crockford base32)01JQ8ZK4T0XA9V7C2M5N3PQRSD^^^^^^^^^^ first 10 characters are time, the other 16 are randomThe big-endian layout is a requirement rather than a stylistic choice. Only in that byte order does a byte-wise comparison of the encoded form agree with a numeric comparison of the timestamp, which is the entire point. A little-endian v7 would look identical to the eye and shuffle arbitrarily, losing its one advantage. Ordering within a single millisecond is guaranteed too: the 12 bits of rand_a act as a counter, so a hundred ids minted in a tight loop still come out sorted.
| Property | UUID v4 | UUID v7 | ULID |
|---|---|---|---|
| Length | 36 characters | 36 characters | 26 characters |
| Sortable | No | Yes | Yes |
| Random bits | 122 | 74 | 80 |
| Reveals creation time | No | Yes (millisecond) | Yes (millisecond) |
| Encoding | Hex | Hex | Crockford base32 |
ULID and Crockford base32
A ULID packages the same idea in a different shell: 48 bits of time, 80 bits of randomness, rendered as 26 characters of Crockford base32. The alphabet drops I, L, O and U. The first three because they are visually confusable with 1 and 0 when somebody retypes an id from a screen, and U so the encoding cannot accidentally spell something unfortunate. The result is case-insensitive and contains no hyphens, which makes it comfortable in URLs and in text selected by double-click. The time prefix is exactly 10 characters, so prefix range scans work directly.
Warning
Collision risk and auto-increment integers
With 122 random bits, v4 collisions are not a practical concern: you would need to mint billions of ids before the probability became worth reasoning about. The real risk is a weak source of randomness rather than the arithmetic, since generators built on Math.random() produce values that look like UUIDs while being predictable from prior draws. Every byte here comes from crypto.getRandomValues, and the version and variant nibbles are written explicitly so strict parsers such as the Postgres uuid type accept the result.
Compared with an auto-increment integer, the tradeoffs are clear. Integers are short and easy to read, but anyone who sees /orders/1042 can try /orders/1043, and the numbers disclose your volume. They can also only be produced by the database, so you cannot know a row identity before writing it. With UUIDs you generate the id on the client and build related records in a single round trip, at a cost of sixteen bytes instead of eight.
Frequently Asked Questions
- Should I use v4 or v7?
- Use v7 for database primary keys, where index locality makes a measurable difference to insert throughput. Use v4 wherever exposing the creation time would be a problem, or where unpredictability of the id matters. Converting an existing table needs a real reason, since both fit the same 36-character column.
- Will UUIDs ever collide?
- Not in practice. With 122 random bits in a v4, the probability stays negligible at any volume a normal system produces. The danger is not mathematical collision but a weak randomness source: fake UUIDs built on
Math.random()are both predictable and unevenly distributed. - UUID or auto-increment integer?
- Integers are smaller and easier to read, but they are enumerable, they disclose your row count and ordering, and only the database can mint them. UUIDs allow distributed generation and are not guessable, at twice the storage. Prefer UUIDs for distributed systems and externally visible resource URLs, integers for internal tables behind a single database.
- Is a UUID a secret?
- No. Never use a UUID on its own as a session key, a password reset token or proof of authorization. Even a v4 drawn from a strong source ends up in logs, Referer headers and browser history. When you need a token, build one for that purpose, with expiry and revocation.
- What is a ULID?
- The same idea as UUID v7 in a different encoding: a 48-bit timestamp, 80 bits of randomness, written as 26 characters of Crockford base32. No hyphens, case-insensitive, and the confusable letters I, L, O and U are absent from the alphabet, which makes it easier to handle in URLs and in ids people copy by hand.