UUID Generator

Why UUIDv7 Beats UUIDv4 for Database Primary Keys

For about a decade, the conventional wisdom was: "use auto-increment integers as primary keys, because random UUIDs destroy database performance." This was correct, but the conclusion that "UUIDs are slow" was overgeneralized. The real culprit was UUIDv4's full randomness, not UUIDs as a category. UUIDv7 (standardized in RFC 9562, May 2024, replacing the old RFC 4122) puts a Unix-millisecond timestamp in the high-order bits, then random bits below. Two values generated 1 ms apart sort with the timestamp; two values in the same millisecond sort randomly within that millisecond. The shape of insertions changes completely. The reason this matters is the B-tree. PostgreSQL, MySQL/InnoDB, SQL Server — all of them store rows in B-tree (or B+-tree) order keyed by the primary key. Inserting a value with a primary key larger than any existing key always lands on the rightmost leaf. That page is hot in the buffer pool, the next insert hits the same page, and the database barely needs to do any IO. With UUIDv4, every insert lands on a random leaf — every insert pulls a new page from disk, dirties it, evicts something else, and writes the dirty page back. On a table large enough that the index does not fit in RAM, UUIDv4 inserts can be 10–50x slower than auto-increment, and the B-tree fragments badly because random inserts cause page splits all over the tree. UUIDv7 eliminates this. Inserts are time-ordered, so they land in a small hot region of the index — not always the literal rightmost leaf (the random low bits cause some local jitter) but a tiny working set that stays in cache. Benchmarks from PlanetScale, AWS, and Percona all show UUIDv7 INSERT throughput within 10–20% of bigint auto-increment on modern InnoDB and Postgres. The fragmentation that plagued UUIDv4 vanishes because new keys always sit near each other temporally. There is one practical wrinkle. InnoDB uses 16-byte BINARY storage natively for UUID — store as BINARY(16), not as the 36-character hex string with dashes. The hex form is 36 bytes (or 32 without dashes), more than 2x the storage and more than 2x the index footprint, which translates directly to fewer keys per page and more IO. Postgres has a native uuid type that stores 16 bytes; use it. The standard tradeoff for human-debuggability vs storage is to store binary, render hex only at the API boundary.
// UUIDv4 layout: 122 random bits, 6 fixed bits
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
                ^^^^      ^^^^
                version 4 variant bits

// UUIDv7 layout: 48-bit Unix ms timestamp + 74 random bits
ttttttttttttt-tttt-7xxx-yxxx-xxxxxxxxxxxx
^^^^^^^^^^^^^ ^^^^      ^^^^
unix_ts_ms    rand-A    rand-B
              + version + variant

// Postgres
CREATE TABLE orders (
  id uuid PRIMARY KEY DEFAULT uuidv7(),  -- PG18+
  ...
);

// MySQL — store as BINARY(16), not VARCHAR(36)
CREATE TABLE orders (
  id BINARY(16) PRIMARY KEY,
  ...
);

// JS — generate UUIDv7 (no deps)
function uuidv7() {
  const ts = Date.now();
  const tsHex = ts.toString(16).padStart(12, '0');
  const rand = crypto.getRandomValues(new Uint8Array(10));
  rand[0] = (rand[0] & 0x0f) | 0x70; // version 7
  rand[2] = (rand[2] & 0x3f) | 0x80; // variant
  const r = [...rand].map(b => b.toString(16).padStart(2, '0')).join('');
  return `${tsHex.slice(0,8)}-${tsHex.slice(8)}-${r.slice(0,4)}-${r.slice(4,8)}-${r.slice(8)}`;
}

UUID Collision Probability — The Birthday Paradox in Numbers

"How likely is a UUID collision?" is the question every team asks once. The answer is rooted in the birthday paradox. UUIDv4 has 122 random bits (the other 6 are fixed for version and variant), so the namespace is 2^122 ≈ 5.3 × 10^36 distinct values. The naive intuition says: with that many slots, a collision is impossible. But the birthday paradox says collision risk scales with sqrt(N), not N — the rough rule is that you need to generate about sqrt(2^122) ≈ 2^61 ≈ 2.3 × 10^18 values before the first 50% chance of a collision. Concretely: generate one billion UUIDv4s every second for a hundred years and your collision probability is still on the order of 10^-9. Generate one trillion total and the probability of any collision among them is roughly 10^-15 — about a million times less likely than getting hit by a meteorite this year. For a single application, collision risk is effectively zero. The math only gets interesting if you concatenate UUIDs from many independent systems generating at extreme scale (think a global IoT fleet over decades), and even then the safe practical answer is: pick a strategy and never worry. The version table looks roughly like this for the modern UUID family: v1 (1988): timestamp + MAC address. Unique per host but reveals the host's MAC and time. Mostly retired. v3 / v5: deterministic hashes of a namespace + name. Same input always produces the same UUID — useful for derived IDs. v4: 122 random bits. The dominant choice 2010–2024. v6: like v1 but with a sortable byte order. Niche. v7 (2024 RFC 9562): 48-bit Unix ms timestamp + 74 random bits. Strongly preferred for new systems. v8: custom; you fill in the bits yourself. Use only if v7 does not fit your needs. A practical takeaway: do not hand-roll your own UUID format. The 6 reserved bits (version + variant) make a UUID identifiable by tools and databases. A v4 with the version bits set wrong will be interpreted as some other version, breaking comparison, sorting, or driver-level optimizations. Always use a vetted library or the standard formula above.
// Birthday paradox: probability of ANY collision among n values
// drawn from a space of size N is ≈ 1 - exp(-n^2 / (2N))

// For UUIDv4: N = 2^122
// n = 1 billion → P ≈ 4.7 × 10^-20
// n = 1 trillion (10^12) → P ≈ 4.7 × 10^-14
// 50% collision threshold: n ≈ 2.3 × 10^18

// Detect the version of a UUID
function uuidVersion(uuid) {
  // version is the first hex digit of the third group
  return parseInt(uuid[14], 16);
}
uuidVersion('018fb1c2-...-7abc-...');  // 7
uuidVersion('a1b2c3d4-...-4abc-...');  // 4

// Deterministic UUIDv5 (SHA-1 of namespace + name)
// Same name → same UUID, useful for migration mapping
import { v5 as uuidv5 } from 'uuid';
const NS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // DNS namespace
uuidv5('example.com', NS); // always "cfbff0d1-9375-5685-968a-48ce8b50e3e0"

ULID vs UUIDv7 — Pick One and Move On

Before UUIDv7 was standardized, the community had already invented several time-ordered ID formats. The most popular is ULID (Universally Unique Lexicographically Sortable Identifier), specced in 2016. A ULID is also 128 bits — 48 bits of millisecond timestamp followed by 80 bits of randomness — but it is encoded in Crockford Base32 instead of hex with dashes. The visual format is 26 characters like 01HX1Y9ZQM7TWBKE6JR5VN8YGB. Functionally ULID and UUIDv7 are very similar. Both are time-ordered, both have a millisecond timestamp prefix, both have plenty of random bits, both sort lexicographically when stored as text. The differences are aesthetic and ecosystem: Encoding: ULID is Crockford Base32 (26 chars). UUIDv7 is hex with dashes (36 chars). ULID strings are shorter and intentionally avoid easily-confused characters (no I, L, O, U). For URLs and human reading, ULID wins. Database support: UUIDv7 is a UUID, so every database with a uuid type stores it efficiently as 16 binary bytes. ULID has no native type — you store it as CHAR(26) or BINARY(16) and convert at the boundary. Tooling: UUID is the lingua franca. Every language, every ORM, every database has decades of UUID tooling. ULID has good libraries but spotty tool integration. Spec maturity: UUIDv7 is RFC 9562 (May 2024, IETF standard). ULID is a community spec on GitHub. The 2026 default is UUIDv7 unless you have a specific URL-aesthetic reason to prefer ULID. UUIDv7 carries the institutional momentum of being a real RFC, native database support is real and growing (Postgres 18+, MySQL 9+, all major drivers), and "just use UUID" matches what every code reviewer and DBA already understands. If you need URL-friendly short IDs for end-user-visible things (think YouTube's /watch?v= or Stripe's cus_), neither UUIDv7 nor ULID is ideal — those use NanoID-style alphanumeric tokens that are 21 characters and pure random. Different tool for a different job. Use sortable IDs internally as primary keys, NanoIDs externally as customer-facing identifiers.
// Side-by-side
UUIDv7: 018fb1c2-7abc-7d3e-8f4a-1b2c3d4e5f60   (36 chars, hex)
ULID:   01HX1Y9ZQM7TWBKE6JR5VN8YGB              (26 chars, base32)
NanoID: V1StGXR8_Z5jdHi6B-myT                    (21 chars, alphanumeric)

// Both UUIDv7 and ULID sort by time when stored as strings:
SELECT id FROM orders ORDER BY id; -- chronological without an extra index!

// UUIDv7 Postgres setup
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- for older versions
CREATE TABLE orders (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- v4
  -- or in PG18+:  DEFAULT uuidv7()
  user_id uuid REFERENCES users(id),
  created_at timestamptz NOT NULL DEFAULT now()
);

// Generating ULID
import { ulid } from 'ulid';
ulid();                       // "01HX1Y9ZQM7TWBKE6JR5VN8YGB"
ulid(1714560000000);          // ULID for a specific timestamp
Last updated:

About this tool

A UUID generator produces 128-bit identifiers in the standard 8-4-4-4-12 hex format. UUID v4 is fully random; v1 includes a timestamp and MAC address; v7 (newer) embeds a Unix-millisecond timestamp at the front so the IDs sort chronologically — a property databases love because it keeps inserts on the rightmost B-tree page.

How to use

  1. Pick the version: v4 for general-purpose IDs, v7 for database primary keys, v1 if you need MAC + time.
  2. Choose how many UUIDs to generate at once.
  3. Click Generate — the IDs render below in a clean copy-friendly list.
  4. Click any single ID to copy it, or use Copy All for the entire batch.
  5. Drop the values into your migration file, fixture, environment variable, or test.

Common use cases

  • Generating primary keys for a new database table where auto-incrementing integers leak business volume.
  • Producing correlation IDs for distributed-tracing logs.
  • Seeding test fixtures with stable, unique IDs.
  • Creating idempotency keys for safe API retries.
  • Naming files in object storage so two clients never collide.
  • Replacing sequential URLs with hard-to-guess identifiers for shared links.

Frequently asked questions

Q. When should I use v7 instead of v4?

A. When you store UUIDs as primary keys in a database. v7 sorts by time, which keeps B-tree inserts hot and avoids the random-write penalty v4 inflicts on row storage.

Q. Is UUID v4 really unique?

A. Mathematically, the chance of a collision after billions of generations is still essentially zero — provided your random source is cryptographic, which crypto.getRandomValues guarantees.

Q. Why does UUID v1 expose a MAC address?

A. It was designed before privacy was a concern. Most modern v1 implementations randomise the node field, but if you care about privacy prefer v4 or v7.

Q. Are UUIDs case-sensitive?

A. No. Hex digits a-f and A-F mean the same thing. Most implementations emit lowercase; both compare equal.