UUID 550e8400-e29b-41d4-a716-446655440000 is a 128-bit ID you can generate on any machine without asking a database — 3.4×10³⁸ possibilities, collision effectively impossible. This guide explains what a UUID is, its 8-4-4-4-12 anatomy, the 5 versions (v4 random is the default, v7 time-ordered for DBs), why every system from Postgres primary keys to S3 objects uses one, and pitfalls like string vs binary storage.
- What: Universally Unique Identifier — 128-bit = 32 hex chars + 4 dashes
550e8400-e29b-41d4-a716-446655440000(36 text, 16 bytes binary) per RFC 4122 / RFC 9562. - Why: Generate locally, globally unique without coordination — no DB lock, no central counter, works offline, scales distributed. Auto-increment 1,2,3 leaks count and needs roundtrip; UUID is opaque and safe to expose.
- Structure: 8-4-4-4-12 hex groups: time-low, time-mid, time-hi+version (digit 1/3/4/5/7), clock-seq+variant (8/9/a/b), node. 128 bits include 6 fixed overhead → 122 random for v4.
- Versions: v1 time+MAC (sortable but MAC leak), v4 random (122 bits random, default — use this), v3 MD5 / v5 SHA1 name-based (deterministic same input → same UUID), v7 Unix ms + random (time-ordered, best for DB PK per draft).
- Pitfall: Random v4 fragments B-tree index — use v7 for primary key; store as 16-byte binary not 36-char text for half size. Generate correct v4 via our random UUID generator (one click, bulk, no reload) or
crypto.randomUUID().
What Is a UUID — 128-Bit ID With No Central Registry
UUID is a 128-bit label (16 bytes) rendered as 32 hex digits in 5 hyphenated groups 8-4-4-4-12: 550e8400-e29b-41d4-a716-446655440000. Lowercase, variant 8/9/a/b in third group's leading nybble, version digit (4 for v4) in 41d4.
Total possibilities: 2¹²² ≈ 5.3×10³⁶ for v4 (≈ 3.4×10³⁸ total 128) — need billions of IDs for 1% collision per birthday bound — effectively zero. So any machine can pick one without asking a central ID server, unlike auto-increment which needs SELECT MAX(id)+1 lock.
Comparison: Auto-increment 1,2,3 reveals row count, requires DB roundtrip before insert, and sharding needs coordination. UUID 550e8400... is opaque, generates in app before DB, works offline, sharded. Downsides: 36-char text larger than 4-byte int, random v4 not sortable. See RFC 4122 and new RFC 9562 (UUID v6/v7/v8).
UUID vs GUID
Same spec — GUID is Microsoft name for RFC 4122 UUID (often braces {550e8400-...} and uppercase). Use lowercase hyphenated for web per RFC.
Anatomy — 128 Bits = 32 Hex + 4 Dashes = 16 Bytes
550e8400 - e29b - 41d4 - a716 - 446655440000
time-low time-mid time-hi+ver clk+var node
8 hex 4 hex 4 hex(1st is version) 4 hex 12 hex
version 4 (random) variant 8/9/a/b (RFC 4122 standard)
Hex view is just 16 bytes rendered as 32 hex: each pair hex = one byte. Binary size 16 bytes, text size 36 bytes (twice) — store binary in DB for half disk if you have many. In Postgres uuid type is 16 bytes binary, in MySQL BINARY(16) or CHAR(36) text — choose binary for index. See Postgres UUID type.
Variant and Version Nibbles
Variant nybble (13th hex char, first after second dash's third group) must be 8/9/a/b for standard: a716 → a is variant. Version nybble (15th hex char, after second dash's first char) → 41d4 → 4 is v4. Check via regex ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$.
Versions — v1, v3, v4, v5, v7 (Which to Use)
| Version | How Built | When |
|---|---|---|
| v1 Time+MAC | 60-bit timestamp + clock + node MAC | Sortable but MAC leak + clock collision → avoid unless legacy |
| v4 Random (default) | 122 random bits + 6 fixed | Generic ID — no coordination, default choice per RFC 4122 |
| v3 MD5 / v5 SHA1 | Namespace + name hashed → UUID | Deterministic: same ns:DNS + "example.com" → same UUID (v5 preferred over v3 due to SHA1 vs MD5) |
| v7 Time-ordered (new) | 48-bit Unix ms + 74 random | DB primary key — sortable by time, random tie-break — see RFC 9562 v7 |
Rule: need random opaque ID → v4 (or v7 if DB PK). Need reproducible ID from name (e.g., file path → same UUID) → v5 with namespace 6ba7b810-9dad-11d1-80b4-00c04fd430c8 (DNS). Need time-ordered for index → v7 beats v1 (no MAC, monotonic). v1 still appears in legacy Java UUID.nameUUIDFromBytes confusion.
v7 Why Now?
v7's 48-bit millisecond timestamp (like Snowflake) gives sortability: 0189a3b2-550e-7... sorts by time, so B-tree insert is sequential not random — no fragmentation. v4 random inserts at random leaf → page splits. Draft v7 spec requires monotonic counter if multiple per ms.
Where Every System Uses UUID — DB Keys, Files, APIs, Distributed
- DB Primary Keys: Postgres
CREATE TABLE users (id uuid PRIMARY KEY DEFAULT gen_random_uuid())→uuid16-byte binary type; MySQLUUID_TO_BIN(UUID()). NoSERIALlock, works offline, sharded without central allocator. See Postgres UUID. - Files and S3: S3 key
uploads/550e8400-.../photo.jpgglobally unique, no temp file collision on parallel workers. - APIs and Tracing:
X-Request-ID: 550e8400-...per request for distributed trace — generate at edge, propagate, log. See X-Request-ID. - Offline and Edge: Service worker generates offline, syncs later — no central ID needed. PWA, mobile.
Auto-increment's downsides drive adoption: sequential leaks business metric (order 1000 → 1001 reveals volume), needs DB roundtrip before you know ID (can't create child rows offline), sharding needs allocator. UUID is opaque, pre-generatable, globally safe.
UUID vs ULID vs Snowflake
UUID v4 36-char text, ULID 26-char Crockford base32 time-ordered sortable, Snowflake 64-bit int time-ordered (Twitter). Use UUID for standard interop, ULID for URL-short time-ordered, Snowflake for 64-bit numeric PK. UUID v7 is standard time-ordered answer.
Pitfalls — DB Index, String vs Binary, and Collision Myth
DB Index: Random v4 as PK causes B-tree fragmentation — inserts random leaf → page splits, 30% slower after 10M rows. Fix: use v7 time-ordered for PK (sequential insert at end), or keep v4 but add created_at index for time queries. Benchmark Postgres shows v7 sequential inserts 2× faster than v4 random at scale.
String vs Binary: CHAR(36) text 36 bytes vs BINARY(16) 16 bytes — half disk and RAM for index. In MySQL 8.0.17+, use UUID_TO_BIN(uuid, 1) swap time bytes for sequential. In JS, store as string for interop, binary in DB for index — convert at boundary.
Collision myth: 2¹²² ≈ 5×10³⁶; birthday bound: need ~2⁶¹ ≈ 2×10¹⁸ IDs for 1% collision — at 1B IDs/sec, 60 years to 1%. Effectively zero — don't add dedup check. Math per Wikipedia collisions.
Generate Correctly — Not Math.random
// Correct: crypto-grade
// JS (browser + Node 19+):
crypto.randomUUID() // v4
// Node older:
import { randomUUID } from 'crypto'; randomUUID()
// Python:
import uuid; uuid.uuid4() // or uuid7 via uuid_extensions
// Java:
UUID.randomUUID() // v4
// Shell:
uuidgen -r # -r random v4
// Postgres:
SELECT gen_random_uuid(); // pgcrypto
Don't use Math.random() — 53-bit, not 122, and not crypto — collision higher, not RFC. Use our random UUID generator for one click bulk (100 at once, no reload, copy) — it uses crypto.randomUUID() correctly with variant/version bits set.
Tools — Generate, Validate, Bulk (Sharp)
Generator page shows 550e8400-e29b-41d4-a716-446655440000 with copy, regenerate, bulk 100, uppercase toggle, and v4 vs v7 switch. Validator checks regex ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ plus version digit and lowercase normalization. For code bulk: for (let i=0;i<100;i++) console.log(crypto.randomUUID()) → paste into DB seeder.
Deep Anatomy — Version and Variant Bits in Hex
Inspect 550e8400-e29b-41d4-a716-446655440000 per nibble:
550e8400-e29b-41d4-a716-446655440000
|------| |--| |--| |--| |----------|
time_low time_mid time_hi_ver clk_seq node
version digit = 4 here (41d4)
variant = a (10xx → 8/9/a/b)
128 bits = 32 hex + 4 dashes; 16 bytes binary. Text "550e..." is hex dump of those 16 bytes — like hex dump.
Variant 8 binary 1000, 9 1001, a 1010, b 1011 all start 10xx per RFC 4122 standard — ensures 550e... parses as standard variant, not reserved. See RFC 4122 variant.
Binary vs Text — Half Size
Store 550e8400-e29b-41d4-a716-446655440000 as 36-char text CHAR(36) vs 16-byte binary BINARY(16) — half. In Postgres uuid is binary 16, in MySQL 8+ UUID_TO_BIN(uuid, 1) swaps time bytes for sortable v7 index. In JS, receive as string for interop, convert to binary at DB boundary — don't store as VARCHAR(36) if you have millions. Benchmark via Postgres UUID storage note.
Versions Deep — v1 Time+MAC, v4 Random, v5 SHA1, v7 Time-Ordered
v1 (time + MAC, RFC precedent): 60-bit timestamp (100ns since 1582) + 14-bit clock + 48-bit node MAC → sortable by time but MAC leaks hardware. Clock may collide if node rewinds — hence 14-bit clock. Legacy — v1 spec. Avoid unless you need time sort with legacy MAC tie.
v4 (random, default): 122 random bits (6 fixed overhead) via crypto.randomUUID() — no coordination, best for generic. Standard per v4 spec. Collision bound above.
v3 (MD5) / v5 (SHA1) name-based: hash namespace UUID + name → UUID: deterministic same ns:DNS + "example.com" → same UUID. v5 preferred (SHA1 vs MD5). Example: uuidv5("example.com", DNS_NAMESPACE) → 9073926b-... same every run. Use for reproducible ID from file path, not random.
v7 (time-ordered, new RFC 9562): 48-bit Unix ms timestamp + 74 random with 12-bit sub-ms counter — sortability of v1 without MAC, random tie-break. Best for DB PK: B-tree inserts sequential at end, not random leaf. Required monotonic counter if multiple per ms per v7 spec. Draft RFC 9562 adds v6 (reordered v1), v7, v8 (custom).
Choice Flowchart
Need random opaque? → v4 (or v7 if DB PK)
Need same name → same UUID? → v5 (namespace + name)
Need time-ordered PK? → v7 (not v1)
Legacy time+MAC? → v1
Where Else — ULID, Snowflake, Nano ID
UUID not alone: ULID 26-char Crockford base32 time-ordered sortable, Snowflake 64-bit int time-ordered (Twitter) — 64-bit numeric PK, Nano ID short V1StGXR8_Z5j. Use UUID for standard interop (RFC), ULID for URL-short time-ordered, Snowflake for 64-bit numeric PK.
Collision Math — Why "Infinite" Is Practical
Birthday paradox: after n IDs, collision prob ≈ 1 - exp(-n² / 2×2¹²²). At 1B IDs/sec for 100 years, n≈3×10¹⁸, prob ≈0.01% — need 10²⁷ for 50%. So generate without dedup check — check only if paranoid. See collisions and birthday problem.
// Create child rows offline without DB roundtrip
const id = crypto.randomUUID() // 550e8400-...
await db.users.insert({ id, name: "Ada" })
await db.posts.insert({ id: crypto.randomUUID(), userId: id, title: "Hi" }) // foreign key known before insert
// No SELECT MAX+1 lock, no sequence
App-generated before insert → can build graph offline, sync later.
Real Code — Generate, Validate, Bulk, and Store Correctly
Generate: one line per language (MDN randomUUID):
// JS (browser + Node 19+): crypto.randomUUID() → "550e8400-e29b-41d4-a716-..."
// Node older: import { randomUUID } from 'crypto'; randomUUID()
// Python: import uuid; uuid.uuid4() # or uuid7 via uuid_extensions
// Java: UUID.randomUUID() // v4
// Go: github.com/google/uuid -> uuid.New().String()
// Rust: uuid::Uuid::new_v4()
// Shell: uuidgen -r # v4 random
// Postgres (pgcrypto): SELECT gen_random_uuid();
// MySQL 8: SELECT UUID() # string 36; for binary: SELECT UUID_TO_BIN(UUID(), 1)
Validate: regex plus version/variant as above — ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ per RFC 4122 Section 3 plus lowercase normalization.
Bulk: for (let i=0;i<100;i++) console.log(crypto.randomUUID()) → file; DB seeder loops 1K.
Store — Text vs Binary in DB
In Postgres use uuid type (16-byte binary) not VARCHAR(36): CREATE TABLE events (id uuid PRIMARY KEY DEFAULT gen_random_uuid()) → index is binary, half size, faster. In MySQL 8.0.17+ use BINARY(16) + UUID_TO_BIN(uuid, 1) swap time bytes for sequential v7 index (first byte time). See MySQL UUID swap flag. In JS API, return string "550e..." for interop — convert at DB boundary: uuidToBin(string) on write, binToUuid(bin) on read.
Pitfalls Deep — Uppercase, Braces, URN, and Time Sort
- Uppercase vs Lowercase: RFC prefers lowercase
550e8400— uppercase550E8400is case-insensitive but canonical lowercase via.toLowerCase()before compare/store. - Braces and URN: Microsoft GUID
{550e8400-...}and URNurn:uuid:550e8400-...are presentation only — strip braces/URN before storing, store hyphenated lowercase. - Time sort: v4 random not sortable by time —
ORDER BY idrandomizes. Need time-ordered PK? Use v70189a3b2-550e-7...where first 48 bits are timestamp →ORDER BY id≈ORDER BY created_at.
-- Random v4 PK after 10M rows: B-tree fragments, inserts random leaf → 30% slower, index 2× larger
-- Time-ordered v7 PK: inserts sequential at end → no splits, index sequential, time queries covered
-- Keep v4 for opaque external ID, v7 for PK if you need time order
CREATE TABLE orders (id uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), -- or app generates v7
external_id uuid DEFAULT gen_random_uuid() );
Use v7 for PK if you order by time often.
Real Stacks — Two Teams That Pick Differently
API team (REST): POST /users body {"name":"Ada"} → server returns {"id":"550e8400-e29b-41d4-a716-...","name":"Ada"} where id was crypto.randomUUID() before DB insert — client can optimistically show ID. Frontend stores X-Request-ID per tracing.
Data team (ETL): S3 objects s3://bucket/events/550e8400-.../data.json where key is UUID v4 — no temp file collision on parallel workers, Hive partition not needed.
Both avoid sequential leaks: API's UUID doesn't reveal users 1000 → 1001 business metric; ETL's UUID doesn't need central allocator like Snowflake's Zookeeper.
Security — Not Random Is Predictable, Not a Secret
UUID v4 must be crypto-grade — Math.random() is 53-bit with seed leak → brute force. Use crypto.randomUUID() (getRandomValues) per MDN. But UUID is identifier, not secret — don't use as password even though 122 bits looks strong; it lacks KDF and rotation. Use for ID, use crypto.randomBytes + nanoid for token.
Leak Check — Don't Log Full UUID If Private?
UUID itself is public identifier, not PII, but don't log it with PII in same line if you treat logs as redacted. Redact userId=550e... if GDPR requires pseudonymization — hash it.
for id in $(cat ids.txt); do [[ $id =~ ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ ]] || echo "bad $id"; done
jq -r '.id' api.json | grep -E '^[0-9a-f-]{36}
What is a UUID?
128-bit ID text xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx (M version 1-8, N variant 8/9/a/b) with 122 random bits for v4 — globally unique without central registry, per RFC 4122 / RFC 9562 — used for DB keys, files, APIs.
Are UUID and GUID the same?
Same spec — GUID is Microsoft name for UUID (often {550e8400-...} braces, uppercase). Use lowercase hyphenated for web interop.
Which UUID version should I use?
Random opaque → v4 (or v7 if DB primary key for time-order). Deterministic from name → v5 (namespace+name). Time-ordered DB → v7. Legacy v1 only if you need time+MAC compatibility.
Will UUIDs collide?
Practically no — 2¹²² possibilities → birthday 1% collision needs 2×10¹⁸ IDs. At 1B/sec, 60 years. No dedup needed; just use crypto generator.
Should I store UUID as string or binary?
Binary 16 bytes in DB for half size and faster index (uuid type Postgres, BINARY(16) MySQL with UUID_TO_BIN(...,1)), string 36 chars for JSON API interop — convert at boundary. Don't store as VARCHAR(36) if you have millions.
How do I generate a UUID in JavaScript?
Browser + Node 19+: crypto.randomUUID() → "550e8400-e29b-41d4-a716-446655440000". Bulk: loop 100 with that call. For older Node: import { randomUUID } from 'crypto'.
&& echo ok
Fail CI if any ID is uppercase or missing dashes — canonical lowercase hyphenated.
History — From Apollo to RFC 9562
UUIDs came from Apollo Network Computing System (1980s) for distributed without central — DCE 1.1 (1997) → RFC 4122 (2005, Leach) → RFC 9562 (2024, Peabody) adding v6/v7/v8 and clarifying. v7's Unix ms proposal fixes v1's 100ns 1582 epoch oddity. See RFC 4122 history.
Common Questions — Top and Bottom Handling
Top and bottom? No — UUID has no top/bottom, but v7's first 48 bits are timestamp — top time. Want time range query? WHERE id BETWEEN uuidv7_start('2024-01-01') AND uuidv7_end('2024-01-02') via timestamp, not random v4.
Copy working 550e8400-e29b-41d4-a716-446655440000 template — one correct reused beats four hand-typed with different missing dashes.
Bonus: keep uuid column + created_at — ORDER BY created_at even with v7, so time queries don't rely on ID sort.
Version your uuid generation — v4 for opaque external ID, v7 for PK if you need time sort — so git diff shows which ID was added when created_at changed.
Keep one source — committed gen_random_uuid() for DB default, crypto.randomUUID() for app — both RFC 4122, not Math.random().
Validate early, fail fast at parse — not at runtime when uuid string missing dashes crashes API.
Keep one source — committed gen_random_uuid() vs crypto.randomUUID() — choose per context.
Copy working 550e8400-e29b-41d4-a716-446655440000 template — one correct reused beats four hand-typed with different missing dashes.
Version your uuid generation — v4 for opaque, v7 for time-ordered PK — so git diff shows which was added.
Keep uuid generation — crypto.randomUUID() for app, gen_random_uuid() for DB — both RFC 4122.
Validate at build — regex fails fast before deploy.
Keep one source — committed gen_random_uuid() vs crypto.randomUUID() — choose per context.
Frequently Asked Questions
What is a UUID?
128-bit ID text xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx (M version 1-8, N variant 8/9/a/b) with 122 random bits for v4 — globally unique without central registry, per RFC 4122 / RFC 9562 — used for DB keys, files, APIs.
Are UUID and GUID the same?
Same spec — GUID is Microsoft name for UUID (often {550e8400-...} braces, uppercase). Use lowercase hyphenated for web interop.
Which UUID version should I use?
Random opaque → v4 (or v7 if DB primary key for time-order). Deterministic from name → v5 (namespace+name). Time-ordered DB → v7. Legacy v1 only if you need time+MAC compatibility.
Will UUIDs collide?
Practically no — 2¹²² possibilities → birthday 1% collision needs 2×10¹⁸ IDs. At 1B/sec, 60 years. No dedup needed; just use crypto generator.
Should I store UUID as string or binary?
Binary 16 bytes in DB for half size and faster index (uuid type Postgres, BINARY(16) MySQL with UUID_TO_BIN(...,1)), string 36 chars for JSON API interop — convert at boundary. Don't store as VARCHAR(36) if you have millions.
How do I generate a UUID in JavaScript?
Browser + Node 19+: crypto.randomUUID() → "550e8400-e29b-41d4-a716-446655440000". Bulk: loop 100 with that call. For older Node: import { randomUUID } from 'crypto'.