Skip to content
yutils

UUID vs ULID — Which Identifier Should You Use?

UUID v4, UUID v7, ULID, and Snowflake compared on sortability, database performance, URL friendliness, and collision risk.

~8 min read

Picking an identifier scheme has more options than people realize: UUID v4, UUID v7, ULID, Snowflake, nanoid — each trades off on sortability, database friendliness, URL safety, and predictability. This guide compares the five you'll actually consider and gives a recipe for which one to pick when.

Why the choice matters

Identifiers are expensive to migrate. Once an ID format is baked into URLs, logs, analytics, and backups, swapping it out is a multi-quarter project. And the shape of an identifier — random vs sortable, short vs long, client- vs server-generated — affects:

  • Index efficiency. B-tree indexes love sorted keys. Random UUID v4 as a primary key scatters insertions across the index and tanks write throughput and cache locality.
  • URL safety. Auto-increment integers leak record counts and invite enumeration. Random-enough IDs avoid both.
  • Debuggability. Time-sortable IDs let you eyeball recency in logs without a separate timestamp column.

The five contenders

1. UUID v4 (random)

f47ac10b-58cc-4372-a567-0e02b2c3d479
  • Layout: 122 bits of randomness + 6 bits version/ variant. 128 bits total.
  • Format: 32 hex chars + 4 dashes = 36 chars.
  • Sortable: No.
  • Collisions: Effectively zero. Generate a trillion and the expected collisions sit around 0.0000003.
  • Pros: Generate anywhere — client, server, DB — with no coordination. Universal library support.
  • Cons: Fragments DB indexes. Long.

2. UUID v7 (timestamp + random)

018f5b86-7c4d-7abc-9def-0123456789ab
  • Layout: 48-bit Unix millisecond + 12-bit random + 62-bit random. Standardized in RFC 9562 (2024).
  • Format: Same 36 chars as v4.
  • Sortable: Yes, by millisecond. Random within the same ms.
  • Collisions: 74 random bits within a ms — effectively zero.
  • Pros: Drop-in UUID compatibility plus sortability. Friendly to DB indexes. The recommended default for new systems.
  • Cons: Leaks creation time — if hiding "when was this made" matters, use v4 or store the time separately.

3. ULID

01HQXM5K3D7Z9YW0123456789AB
  • Layout: 48-bit ms + 80-bit random = 128 bits.
  • Format: Crockford Base32, 26 chars, no dashes.
  • Sortable: Lexicographically. A plain string sort gives time order.
  • Pros: URL-friendly (alphanumeric only). 28% shorter than UUID. Convertible 1:1 to UUID (both are 128 bits).
  • Cons: Not a native DB type — usually VARCHAR(26) or BINARY(16). UUID v7 standardization eroded some of ULID's edge.

4. Snowflake (Twitter)

1768956123987456000
  • Layout: 41-bit timestamp + 10-bit machine id + 12-bit sequence = 63 bits (signed 64-bit).
  • Format: Integer, ~19 chars when stringified.
  • Sortable: Yes.
  • Pros: Fastest indexes (integer). Stores in 8 bytes as BIGINT. Short URLs.
  • Cons: Needs machine-id assignment infrastructure. Must handle clock rollbacks. Cannot generate on clients without machine-id collisions.

5. nanoid (short random)

V1StGXR8_Z5jdHi6B-myT
  • Layout: 21 chars default, 126 bits of randomness, URL-safe alphabet.
  • Sortable: No.
  • Pros: Shorter than UUID v4 with equivalent security.
  • Cons: No native DB type. Smaller tooling ecosystem.

At a glance

UUID v4UUID v7ULIDSnowflake
Length363626~19
Random bits122~74 + ms80 + ms~12 + ms
SortableNoYes (ms)Yes (ms)Yes
Leaks timeNoYesYesYes
DistributedYesYesYesNeeds infra
DB typeUUIDUUIDBINARY/VARCHARBIGINT

Picking one

New primary keys

Default to UUID v7. You keep the standard UUID type and get time-sortable indexes for free. Libraries: Node uuid@9+, Python uuid_utils, Go google/uuid@1.6+. yutils' UUID / ULID Generator also generates v7.

Public URLs

If leaking creation time is a concern (signup time, e.g.), use UUID v4 or nanoid. Otherwise UUID v7 or ULID is fine. Never expose auto-increment integers in public URLs — attackers can guess the next id and estimate your record count.

Log / event ids

ULID is the sweet spot — short, sortable, URL-friendly. You can eyeball log order from the id alone, no separate timestamp column needed.

High-throughput PKs

Snowflake or BIGINT auto-increment. UUID v7 is fast enough for most workloads, but the BIGINT (8 bytes) vs UUID (16 bytes) difference matters at the largest scales. Only viable if all IDs are server-generated.

Offline-first / client-generated

UUID v4 or v7. No machine-id coordination, zero collisions. Standard for mobile/PWA apps that create records offline and sync later.

Pitfalls

1. UUID v1 (MAC + timestamp)

Common in old systems but discouraged: leaks the MAC address (privacy) and can collide when generated rapidly on the same machine. UUID v7 is the modern replacement.

2. Storing UUIDs as strings

PostgreSQL and MySQL UUID types are 16-byte binary. Storing the 36-char string form bloats the index 2.25× and slows queries. Always use the native UUID type.

3. ULID monotonicity

ULID is monotonic within a millisecond only if the library's monotonic mode is explicitly enabled. Otherwise it's random within the ms — sort order is guaranteed only at millisecond granularity.

4. Overestimating collision risk

With UUID v4's 122 random bits, generating a million IDs per second for a century still gives you well under a 50% collision chance. Adding UNIQUE constraints and retry loops "just in case" is usually overkill for v4 — but deterministic UUID v5 (seed-based) is a different story.

Try it

Recap

  • Default new PKs to UUID v7 — UUID compatibility + sortable indexes + distributed generation.
  • For public URLs, pick v7/ULID when leaking creation time is fine, v4/nanoid when it isn't.
  • ULID is the sweet spot for log / event ids.
  • Store UUIDs as native binary, not strings.
  • Don't expose auto-increment integers in public URLs — enumeration risk.
Back to guides