UUID v4 vs v7 vs ULID: Which Should You Use?

10 min readDatabases & Identifiers

Why Unique ID Format Matters

Unique identifiers are everywhere — database primary keys, API IDs, request tracking, distributed systems. Choosing the wrong format is invisible at small scale but causes real problems at millions of rows: slower inserts, index fragmentation, and inability to sort by creation time.

UUID v4, UUID v7, and ULID are all 128-bit identifiers — but they differ fundamentally in how those bits are structured.

UUID v4: Pure Randomness

Example: 550e8400-e29b-41d4-a716-446655440000
         xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
         ←—————————— 122 random bits ——————————→

UUID v4 uses 122 bits of cryptographic randomness. It has no structure — you cannot tell when it was created or sort a list by generation order.

The database problem: B-tree indexes expect new values to be larger than old ones (like auto-increment). UUID v4 inserts at a random position in the B-tree, causing page splits and cache misses. At millions of rows, this degrades INSERT performance by 2–10×.

UUID v7: Timestamp + Randomness (RFC 9562)

Example: 018f0c5a-2b4e-7000-8123-456789abcdef
         ←— 48-bit ms timestamp —→ ←— random —→

018f0c5a-2b4e = milliseconds since Unix epoch
7            = version 7
000-8123...  = random bits

UUID v7 is standardized in RFC 9562 (2024). Its 48-bit millisecond timestamp prefix means every new UUID is greater than all previous ones — new rows always insert at the end of the B-tree index, just like auto-increment integers.

Database support: PostgreSQL 17+ has a native uuidv7() function. For earlier versions, use the uuid npm package (v9+) or the pg-ulid extension.

ULID: Lexicographically Sortable

Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV
         ←——— 10 chars ———→ ←—— 16 chars ——→
         48-bit timestamp    80-bit randomness
         (Base32 Crockford encoding)

ULID uses the same timestamp + random structure as UUID v7 but encodes as a 26-character Base32 string. It is shorter, URL-safe, case-insensitive, and guarantees strict monotonic ordering within the same millisecond.

ULIDs are not part of any IETF standard and require a library in every language/database. They cannot be stored in native UUID database columns without conversion.

Head-to-Head Comparison

PropertyUUID v4UUID v7ULID
Format8-4-4-4-12 hex8-4-4-4-12 hex26-char Base32
Length (string)36 chars36 chars26 chars
SortableNoYes (ms precision)Yes (ms + monotonic)
Timestamp encodedNoYesYes
DB insert performancePoor at scaleExcellentExcellent
Native DB UUID typeYesPostgreSQL 17+No (store as text/bytes)
Reveals creation timeNoYesYes
StandardRFC 4122 / 9562RFC 9562 (2024)spec.ulid.io (unofficial)
Library neededNo (built-in)Yes for older DBsYes (always)
URL-safeNo (hyphens)No (hyphens)Yes

Database Performance: The Real Numbers

The difference becomes measurable around 1–5 million rows and significant at 10M+. A typical benchmark on PostgreSQL inserting 10 million rows:

ID TypeInserts/secondIndex sizePage splits
BIGSERIAL~85,000BaselineNone
UUID v7 / ULID~75,000~1.1×Minimal
UUID v4~25,000~1.6×Frequent

Approximate figures — actual numbers vary by hardware, PostgreSQL version, and workload.

Decision Guide: Which to Use?

  • Use UUID v4 when you need to hide creation time (public API IDs exposed to users), for request IDs / nonces where randomness matters, or for non-database identifiers. UUID v4 leaks nothing — UUID v7 and ULID embed a timestamp.
  • Use UUID v7 for database primary keys on PostgreSQL 17+ or when you need RFC-standard sortable UUIDs with good ecosystem compatibility. It fits into existing UUID columns and tooling.
  • Use ULID if you prefer the shorter Base32 format for readability/URLs, need strict monotonic ordering within the same millisecond, or are working in an environment without native UUID v7 support.

Generating Each Format

// JavaScript / Node.js
import { v4, v7 } from 'uuid';       // npm install uuid
import { ulid } from 'ulid';          // npm install ulid

const uuid4 = v4();   // "550e8400-e29b-41d4-a716-446655440000"
const uuid7 = v7();   // "018f0c5a-2b4e-7000-8123-456789abcdef"
const id    = ulid(); // "01ARZ3NDEKTSV4RRFFQ69G5FAV"

// PostgreSQL 17+
SELECT uuidv7();  -- native UUID v7

-- PostgreSQL < 17 (using pg extension)
SELECT gen_random_uuid();  -- UUID v4 only; add extension for v7

// Python
import uuid
uuid4 = str(uuid.uuid4())  # "550e8400-..."
uuid7 = str(uuid.uuid7())  # Python 3.14+ stdlib
# or: pip install uuid7

Key Takeaways

  • UUID v4 is unordered — bad for DB primary keys at scale, good when you need to hide creation time
  • UUID v7 adds a timestamp prefix — dramatically better INSERT performance, sorts chronologically
  • ULID is UUID v7 in Base32 — shorter string, strictly monotonic, not an IETF standard
  • For new projects with PostgreSQL 17+, UUID v7 is the default choice for primary keys
  • For public-facing IDs, keep using UUID v4 to avoid leaking creation time

Related Resources