Back to Blog
Developer Tools7 min read·September 11, 2026

UUID Guide for Developers - When and How to Use UUIDs

Complete developer guide to UUIDs. Learn about UUID versions, when to use them, and generate UUIDs online for free. Covers v1, v4, v5, and best practices.

What a UUID Actually Is

A UUID is 128 bits of data, usually written as 32 hexadecimal digits in five dash-separated groups of 8-4-4-4-12:

550e8400-e29b-41d4-a716-446655440000

The dashes carry no information. They exist so humans can compare identifiers by eye. The interesting part is that not all 128 bits are free: 4 bits encode the version and 2 bits encode the variant. You can read both straight off the string.

550e8400-e29b-41d4-a716-446655440000

^ ^

| variant nibble: 8, 9, a or b means RFC variant

version digit: 4 here, so this is a v4 UUID

The version digit is the first character of the third group. The variant is the first character of the fourth group, and for standard UUIDs its top two bits are 10, which restricts that hex digit to 8, 9, a, or b. Anything else is a legacy Microsoft GUID layout or not a conforming UUID at all.

The governing specification is RFC 9562, published in May 2024, which obsoletes the older RFC 4122. RFC 9562 kept versions 1 through 5 unchanged and added versions 6, 7 and 8. A guide that mentions only RFC 4122 predates v7.

Two UUIDs are reserved: the Nil UUID (00000000-0000-0000-0000-000000000000) and the Max UUID (ffffffff-ffff-ffff-ffff-ffffffffffff). Neither carries a version or variant; treat both as "no value" sentinels and never let one reach a database as a real key.

The Versions, and Which Ones Still Matter

VersionHow it's builtSortable by timeLeaksUse it when

v160-bit timestamp + clock sequence + MAC addressNo (low half of the timestamp is stored first)Creation time, host MACLegacy systems only
v3MD5 of namespace + nameNoNothing directlyInterop with existing v3 data
v4122 random bitsNoNothingDefault choice for opaque IDs
v5SHA-1 of namespace + nameNoNothing directlyDeterministic IDs from stable names
v6v1 fields reordered most-significant-firstYesCreation time, optionally MACMigrating a v1 system to sortable keys
v748-bit Unix millisecond timestamp + 74 random bitsYesCreation time (to the millisecond)Database primary keys
v8Entirely vendor-defined layoutDependsDependsYou need a custom scheme and still want the UUID container

In practice you will use two of these. v4 when the identifier should reveal nothing, and v7 when the identifier is a database key and you want index locality. v1 has no advantage over v6 or v7 and its MAC address field is a genuine privacy problem. v3 and v5 arrived together in RFC 4122; v3 is not an earlier generation, just the MD5 half of the pair, and new deterministic IDs should use v5.

v4: Random, and How Random Is Enough

A v4 UUID has 128 bits minus 4 version bits minus 2 variant bits, so 122 bits of randomness. Generate it from a cryptographic RNG, never from Math.random().

// Browsers: secure context only (HTTPS or localhost).

// Node: global crypto from 19.0; before that,

// const { randomUUID } = require("node:crypto"); // since 14.17

const id = crypto.randomUUID();

// "3f8a1e6c-5b2d-4c7a-9e11-8d4f2b6a0c33"

import uuid

print(uuid.uuid4()) # uses os.urandom under the hood

-- PostgreSQL 13 and later, no extension required

SELECT gen_random_uuid();

The collision question comes up constantly, so here is the actual arithmetic. By the birthday bound, a 50% chance of any collision in a 122-bit space arrives after roughly sqrt(2 × 2^122 × ln 2) values, which is about 2.7 × 10^18 UUIDs. If you minted a billion v4 UUIDs every second, you would reach that point after about 86 years. Collision is not a risk you need to engineer around. A broken RNG is, which is the real reason Math.random() is disqualified.

You can produce single or bulk v4 values with the UUID Generator when you need seed data or fixtures rather than runtime generation.

v7: The One You Probably Want for Primary Keys

v7 puts a 48-bit big-endian Unix timestamp in milliseconds into the first six bytes, then the version nibble, 12 free bits, the variant bits, and 62 more random bits. Because the timestamp sits in the most significant position, sorting v7 UUIDs lexicographically as strings or numerically as bytes sorts them by creation time.

Take this v7 value:

019caef4-ba40-7a1c-9f3b-1c2d3e4f5a6b

Strip the dashes, take the first 12 hex digits — 019caef4ba40 — and read them as an integer: 1,772,461,800,000 milliseconds since the Unix epoch, which is 2026-03-02 14:30:00 UTC. You can paste that number into the Timestamp Converter to confirm it.

function v7Timestamp(uuid) {

const hex = uuid.replace(/-/g, "").slice(0, 12);

return new Date(Number(BigInt("0x" + hex)));

}

v7Timestamp("019caef4-ba40-7a1c-9f3b-1c2d3e4f5a6b");

// 2026-03-02T14:30:00.000Z

That decodability is the trade. v7 gives you B-tree friendly inserts and free creation-time ordering; in exchange, anyone holding the ID learns when the row was created to the millisecond. For an internal orders table that is fine. For a public password-reset link it is not — use v4 there, or a dedicated random token from the Password Generator.

v3 and v5: Deterministic IDs From Names

v3 and v5 hash a namespace UUID concatenated with a name, then overwrite the version and variant bits. v3 uses MD5 (128-bit digest, used whole); v5 uses SHA-1 (160-bit digest, truncated to the first 128 bits). The same namespace and name always yield the same UUID on every machine, forever.

The four predefined namespaces from the RFC:

DNS   6ba7b810-9dad-11d1-80b4-00c04fd430c8

URL 6ba7b811-9dad-11d1-80b4-00c04fd430c8

OID 6ba7b812-9dad-11d1-80b4-00c04fd430c8

X.500 6ba7b814-9dad-11d1-80b4-00c04fd430c8

import uuid

uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")

#=> UUID('cfbff0d1-9375-5685-968c-48ce8b15ae17')

Any correct implementation, in any language, returns that same value. That property makes v5 useful for idempotent imports: derive the row's ID from the source system's natural key, and re-running the import updates instead of duplicating.

The security caveat matters here. As of 2026, MD5 and SHA-1 are both broken for collision resistance — MD5 collisions are trivial, and SHA-1 chosen-prefix collisions have been demonstrated in practice since 2019. Neither hash should be used for signatures, certificates, or integrity checks against an adversary. v3 and v5 are still fine for their intended job, because that job is deterministic naming among inputs you control, not tamper resistance. If an attacker can choose the name you hash, do not treat the resulting UUID as unguessable or unforgeable. If you need a digest for actual integrity work, use SHA-256 — the Hash Generator will produce one.

Storing UUIDs Without Wrecking Your Indexes

A UUID is 16 bytes. Stored as a 36-character string it is 36 bytes, plus length overhead, plus a slower comparison on every index probe. Use a native or binary column.

-- PostgreSQL: native type, 16 bytes

CREATE TABLE orders (

id uuid PRIMARY KEY DEFAULT gen_random_uuid(),

created_at timestamptz NOT NULL DEFAULT now()

);

-- MySQL 8: 16-byte binary column

CREATE TABLE orders (

id BINARY(16) PRIMARY KEY,

created_at DATETIME NOT NULL

);

INSERT INTO orders (id, created_at) VALUES (UUID_TO_BIN(UUID(), 1), NOW());

SELECT BIN_TO_UUID(id, 1) AS id FROM orders;

The second argument to UUID_TO_BIN is the swap flag. MySQL's UUID() returns a v1 value whose time-low field sorts first, which scatters inserts across the index; the swap flag reorders the fields so they sort chronologically. If you generate v7 in your application instead, pass 0 — v7 is already in sorted order and swapping would destroy it.

Two rules survive every schema review: index the UUID only where you actually look rows up by it, and never expose a sequential integer key beside it, which reintroduces the enumeration the UUID was meant to prevent.

Choosing Between UUIDs and Integer Keys

UUID (v4/v7)Auto-increment integer

Uniqueness scopeGlobal, across servicesPer table
Generated byClient or app, before insertDatabase, on insert
Size16 bytes4 or 8 bytes
EnumerableNoYes
Merging shardsNo renumbering neededKey conflicts
Insert localityv7 good, v4 poorExcellent
Readable in a support ticketPoorGood

The decisive factor is usually not performance but *who assigns the ID*. If a client, a queue consumer, or an offline mobile app has to know an entity's ID before the database sees it, you need a UUID. If every write goes through one database and nothing is ever merged across regions, an integer key is smaller, faster, and easier to say out loud.

A common middle ground is both columns: a bigint primary key for internal joins and a v4 UUID as the external, API-facing identifier. You pay 16 extra bytes per row and get non-enumerable public URLs.

Quick Checklist

  • Default to v7 for new primary keys, v4 for anything exposed publicly where creation time should stay private.
  • Generate from a CSPRNG. crypto.randomUUID(), os.urandom, gen_random_uuid() — not a language's default random.
  • Store as 16 bytes, not 36 characters.
  • Validate on input with ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ (case-insensitive) — the third and fourth groups pin version and variant. You can try it against real samples in the Regex Tester.
  • Compare UUIDs case-insensitively. The RFC says output lowercase, accept either.
  • Do not reuse a UUID as a secret unless it is v4 from a cryptographic RNG, and even then prefer a purpose-built token.
#uuid generator#uuid#unique identifier#developer guide

Try Our Free Online Tools

100+ free tools for developers, designers, and everyone. No sign-up required.

Browse All Tools