Skip to content

Which random ID should you use?

· 8 min read

"Generate a random ID" has four common answers, and they differ in ways that only show up later — when the table has fifty million rows, or when someone notices your public URLs leak exactly when each record was created. Here is what actually separates UUID v4, UUID v7, ULID, and Nano ID.

The four, briefly

FormatLengthRandom bitsSortable by time
UUID v436 chars122No
UUID v736 chars74Yes
ULID26 chars80Yes
Nano ID21 chars (default)126No

UUID v4

128 bits, of which 122 are random — six are fixed to mark the version and variant. Written as 36 characters with hyphens. Universally supported, understood by every database and language, and the safe default when you have no particular requirement.

Collisions are not a practical concern. You would need on the order of 2.3 × 1018 v4 UUIDs before reaching a 50% chance of a single duplicate. If you are generating them from a proper CSPRNG, the risk is not worth engineering around.

UUID v7

Standardised in RFC 9562 in 2024, and the most useful new option in years. The first 48 bits are a Unix timestamp in milliseconds; the rest is random. The format is still a normal UUID, so any column, library, or tool that accepts a UUID accepts a v7 — but because the high-order bits increase over time, sorting the IDs sorts by creation order.

ULID

The same idea, predating v7: 48-bit millisecond timestamp plus 80 random bits, rendered in Crockford base32 as 26 characters. Shorter than a UUID and case-insensitive, with an alphabet that excludes I, L, O, and U to avoid transcription errors and accidental words.

It is lexicographically sortable as a string, which is genuinely handy in key-value stores. Its disadvantage is that it is a community specification, not an RFC, and it is not a UUID — a uuid column will not take it. Now that v7 exists, ULID's main argument is the shorter, friendlier text form.

Nano ID

Not a UUID at all: 21 characters from a 64-character alphabet (A–Za–z0–9_-), giving 126 bits of entropy — slightly more than a v4 UUID in 15 fewer characters. URL-safe by construction, with no hyphens to break on and no encoding step needed.

Ideal for public identifiers: share links, invitation codes, short-lived tokens. Less suitable as a database primary key, because there is no native column type and you lose both the tooling that understands UUIDs and any time ordering.

The database argument for time-ordered IDs

This is the reason v7 and ULID exist, and it is worth understanding rather than cargo-culting.

Databases store primary keys in B-tree indexes. Insert a sequential key and every new row lands at the right edge of the tree — the same few pages stay hot in memory, and the index grows by appending. Insert a random key and each row goes to an arbitrary leaf, so the database must read that page, split it when full, and keep a working set that grows toward the size of the whole index.

The effect is worst where the primary key is clustered, meaning the table rows are physically stored in key order — MySQL's InnoDB, and SQL Server by default. There, random primary keys cause page splits and fragmentation in the table itself, not just an index. PostgreSQL's heap storage is less affected, though the index still suffers.

Switching from v4 to v7 is a one-line change that removes the problem, and pagination gets easier too: ordering by ID becomes ordering by creation time, with no separate created_at index.

The privacy argument against them

Time-ordered IDs embed a timestamp, and that timestamp is readable by anyone holding the ID. If IDs appear in URLs, a customer can see precisely when their account was created — and when other records were, if they can observe any. Two IDs reveal the interval between the events. Aggregated across a service, that is a measurable signal about your growth rate and volume, which is exactly the sort of thing competitors extract from public identifiers.

There is also enumeration. Time-ordered IDs are not sequential in a guessable sense — the random component still prevents you from computing a neighbour — but they narrow the search space considerably compared with fully random ones.

The clean resolution is to use different IDs for different jobs: a time-ordered v7 as the internal primary key, and a separate random public identifier — Nano ID or v4 — in URLs. Two columns, no leakage, and the index behaviour you want.

Choosing

  • Database primary key → UUID v7. Ordered inserts, still a UUID.
  • Public URL or share link → Nano ID, or v4 if a UUID is expected.
  • Maximum compatibility, no other requirement → UUID v4.
  • Key-value store where keys sort as strings → ULID.
  • Anything security-sensitive — session tokens, password reset links → do not use any of these as a secret. Use a purpose-built random token with enough entropy, and store only a hash of it.

Generating them without a round trip

There is a small irony in fetching random identifiers from a website: the server generated them, so the server has seen them. For anything that ends up as a token or a hard-to-guess URL, that alone is a reason to generate locally.

KeepItLocally's ID generator produces UUID v4, UUID v7, ULID, and Nano ID in your browser, using crypto.getRandomValues — the same CSPRNG your browser uses for TLS, not Math.random(). Nothing is transmitted and nothing is logged, because there is no backend at all. Bulk generation works offline too.

Quick reference

  • v7 for keys, Nano ID for URLs covers most applications.
  • Random keys fragment clustered indexes — worst on InnoDB.
  • Time-ordered IDs leak creation times — keep them internal.
  • None of these are secrets. Identifiers are not tokens.

Need an actual secret instead? See generating a strong password.