uuiduliddatabasesidentifiers

UUID v7 and ULID: unique identifiers that also sort by date

Why random UUID v4 punishes your database indexes and how UUIDv7 and ULID solve it with sortable-yet-unique IDs. Full comparison and code.

August 24, 2026·7 min read

Random UUID v4 has been the default choice for fifteen years, and in modern databases it turns out to be an expensive decision: identifiers arrive in completely random order, fragmenting B-tree indexes and wrecking the page cache. UUIDv7 — officially standardized in RFC 9562 (2024) — and ULID attack that problem from slightly different angles. Here's the comparison you're missing before choosing a primary key.

The v4 problem: inserts at random

A B-tree keeps data ordered by key. When every new row carries an unpredictable ID, its insertion position is random:

  • The destination page may not be in memory → disk read.
  • It may be full → page split.
  • The buffer pool fills with scattered hot pages used only once.

In PostgreSQL this has a known name: index bloat and write amplification. With millions of daily rows, the difference versus sequential IDs is measurable in orders of magnitude of writes. Auto-incrementals avoided the problem... at the cost of revealing business volume in URLs (/orders/48392 says how many orders you have) and requiring central coordination to assign IDs.

UUIDv7: timestamp + randomness

Version 7 packs a 48-bit Unix millisecond timestamp followed by 74 random bits:

018f 6a1c b2d4 7xxx yxxx xxxxxxxxxxxx
└── timestamp ms ──┘└─ rand_a + rand_b ─┘

Resulting properties:

  • Chronologically sortable: two v7s generated at different moments sort the same as strings and as bytes. ORDER BY idORDER BY created_at without an extra column.
  • Nearly sequential insertion: new records always land "at the end" of the index. Goodbye random splits.
  • Unique without coordination: random bits guarantee uniqueness even across machines generating thousands per millisecond.
  • Free date: decoding the first bytes reveals when the record was born.

Native generation already available:

crypto.randomUUID();          // always v4 (for now)
// v7 via library or manual implementation:
function uuidv7() {
  const ts = Date.now();
  const bytes = crypto.getRandomValues(new Uint8Array(10));
  const hex = [
    ts.toString(16).padStart(12, "0").slice(0, 12),
    [...bytes].map(b => b.toString(16).padStart(2, "0")).join("")
  ].join("");
  // sets version 7 and variant per RFC 9562
  return `${hex.slice(0,8)}-${hex.slice(8,12)}-7${hex.slice(13,16)}-a${hex.slice(17,20)}-${hex.slice(20,32)}`;
}

PostgreSQL 18 ships native uuidv7(); extensions (pg_uuidv7) and client libraries cover earlier versions.

ULID: the cousin with Crockford Base32

ULID (Universally Unique Lexicographically Sortable Identifier) solved the same problem in 2016 with two design differences:

  1. 128 bits all the same, but encoded in 26-character Crockford Base32: 0123456789ABCDEFGHJKMNPQRSTVWXYZ (no I, L, O, U to avoid visual confusion).
  2. 48 bits of timestamp + 80 random, no dashes or visible internal structure:
01ARZ3NDEKTSV4RRFFQ69G5FAV

Practical advantages: 26 characters versus 36, string-sortable in any system without parsing, shorter in URLs. Disadvantage: not an ISO/RFC standard, so ecosystems outside JS/Go/Java need a library, while UUIDv7 is now an international norm with growing runtime and database support.

Direct comparison

Property UUID v4 UUID v7 ULID
Randomness 122 bits 74 bits 80 bits
Temporal order No Yes (ms) Yes (ms)
Text size 36 chars 36 chars 26 chars
Standard RFC 9562 RFC 9562 Own spec
Embedded date No Yes Yes
Native DB support Universal Growing Via extension

Note on the lost bits: v7 keeps statistical uniqueness with room to spare for any real workload; a collision requires matching millisecond AND matching 74 random bits across concurrent generators. Not a practical concern.

When to stick with v4

Not everything is migration: opaque session tokens, exposed API keys, identifiers where filtering by date would leak sensitive info — there, v4's total absence of structure is precisely the virtue. A v7 in public URLs donates its creation timestamp.

And one applied security warning: if you use sortable IDs on public endpoints, pair them with correct authorization — ordering doesn't replace permissions, it just makes neighbors predictable (/invoices/018f6a1c... invites trying the next one).

Generate and verify identifiers

Our UUID generator produces v4 and variants instantly for testing, and if you need a refresher on how v4 works internally with crypto.randomUUID(), see the complete UUID guide in JavaScript.

FAQ

Can I migrate an existing table from v4 to v7? You can use v7 for NEW records without touching old ones: the index accepts both. Reordering history rarely pays off.

Does UUIDv7 leak information? Yes, deliberately: it exposes creation date. If that's a problem in your domain, stay on v4.

What about Twitter's Snowflake IDs? Same idea (timestamp + worker + sequence) but they require worker-ID coordination across machines. v7/ULID achieve the essentials without that infrastructure.


Generate unique identifiers instantly with our online UUID generator, free and right in your browser.

Try it without code

UUID Generator

UUID v4 with crypto.randomUUID().

Open UUID Generator

Built by

Miguel Ángel Colorado Marin (MACM)

Full-Stack Developer · Guadalajara, España

I develop web apps, digital tools and full projects — from design to deployment.

Contact me