UUIDs in Production: What Most Developers Don't Understand
Most developers use UUID v4 without thinking about it. This is a deep-dive into every UUID version, database performance tradeoffs, security misconceptions, and what you should actually be using in production.
You have probably written uuid() or uuid4() hundreds of times without thinking about it. A random string, unique enough, job done.
But if you have ever wondered why your MySQL queries started slowing down after a few million rows, or why your Postgres explain plan showed an unexpected sequential scan, or why a colleague told you to "never expose UUIDs in URLs" — UUIDs are usually somewhere in that story.
This is the article I wished existed when I was first building systems at scale. We are going to cover everything: what a UUID actually is, why version 4 is the wrong default for most databases, the security misconceptions that keep tripping developers up, and what you should actually be using in production in 2026.
What Is a UUID, Actually?
A UUID — Universally Unique Identifier — is a 128-bit value standardised under RFC 4122, updated in 2024 as RFC 9562. It is typically written as 32 hexadecimal characters split into 5 groups with hyphens:
550e8400-e29b-41d4-a716-446655440000
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
Those parts are not arbitrary:
| Segment | Bits | Meaning |
|---|---|---|
550e8400 | 32 | Time-low (in v1) or random (in v4) |
e29b | 16 | Time-mid |
41d4 | 16 | Version encoded in the high nibble (M) |
a716 | 16 | Variant + clock sequence (N) |
446655440000 | 48 | Node (MAC address in v1, random in v4) |
The M nibble is the version number. If you see a UUID starting with ...4... in that position, it is v4. If you see a 7, that is the newer v7.
The Versions — And When to Actually Pick Each One
This is where most articles give you a table and move on. We are not doing that. Here is the honest, opinionated breakdown.
UUID v1 — The Original (and the Privacy Disaster)
v1 encodes a 60-bit timestamp plus your machine's MAC address into the UUID. This made it sortable and traceable back to when and where it was generated.
The problem? Your MAC address is embedded in the ID. Every UUID your server generated leaked the hardware identity of that machine. When this was discovered in production systems it was quietly replaced, but the format survived.
When to use it: Almost never in modern systems. It is still used in some legacy Cassandra setups where time-ordering is needed, but v7 does this better without leaking hardware info.
# Python: what to avoid
import uuid
print(uuid.uuid1()) # leaks your network interface MACUUID v3 and v5 — Deterministic UUIDs From a Name
These are fascinating and vastly underused. Both generate a UUID deterministically from a namespace + name combination. Same input always produces the same UUID.
- v3 uses MD5 (weaker, avoid)
- v5 uses SHA-1 (still acceptable for deterministic IDs)
import uuid
namespace = uuid.NAMESPACE_URL
url = "https://thedanieldallas.com/thoughts"
doc_id = uuid.uuid5(namespace, url)
print(doc_id) # always the same UUID for this URLWhen to use v5: When you need a stable, reproducible identifier for the same conceptual entity across systems. Useful for content hashing, deduplication pipelines, or generating consistent IDs from third-party data you don't control.
UUID v4 — The One Everyone Defaults To (And Why That's a Problem)
v4 is pure randomness. 122 bits of random data. No timestamp, no namespace, no ordering. This is the uuid() call you've made a thousand times.
// Node.js
import { randomUUID } from "crypto";
const id = randomUUID(); // v4# Python
import uuid
id = uuid.uuid4() # v4It is statistically safe — the collision probability across 1 trillion v4 UUIDs is roughly 1 in a billion. But randomness is also its biggest problem in databases.
More on that in the database section. For now: v4 is fine for non-primary key use cases (tokens, reference codes, session identifiers). It is a poor choice as a database primary key on MySQL or any B-tree indexed column at scale.
UUID v7 — The One You Should Be Using for Primary Keys
UUID v7 is relatively new but now formally standardised in RFC 9562 (2024). It encodes a millisecond-precision Unix timestamp in the most significant bits, followed by random data.
018e8f3a-7c2b-7xxx-xxxx-xxxxxxxxxxxx
│ │ │
└── Unix ms timestamp └── random bits
Because the timestamp is at the front, UUIDs generated sequentially are naturally ordered. Row 2 will always sort after row 1. This is the property that makes databases happy.
// Go — using the google/uuid library
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
id, _ := uuid.NewV7()
fmt.Println(id)
}// Node.js — native support in v22+
const { v7: uuidv7 } = require("uuid");
console.log(uuidv7());When to use v7: Essentially any time you would have used v4 as a primary database key. Better ordering, better index performance, still globally unique.
What UUID Does to Your Database (The Part Nobody Talks About)
This is the section that actually matters in production.
The MySQL Clustered Index Problem
MySQL (and MariaDB) use a clustered index for the primary key. This means the physical rows on disk are stored in primary key order. When you insert a new row, MySQL finds the right position on disk and writes it there.
With v4 UUIDs, every new insert lands at a completely random position. MySQL constantly rewrites pages, splits B-tree nodes, and leaves gaps everywhere. This is called index fragmentation, and it hits hard around the 10–50 million row mark:
- Write performance degrades significantly
SELECTqueries need to scan more pages- Buffer pool efficiency drops as the same physical page is rarely reused
-- This is what v4 UUIDs do to your INSERT plan
-- On a 50M row table, this can be 3-5x slower than sequential IDs
INSERT INTO users (id, email) VALUES (uuid(), 'user@example.com');With v7 (or just AUTO_INCREMENT integers), inserts always append to the end. Pages stay full. The index stays tight. Everything is faster.
PostgreSQL Handles This Better — But Not Perfectly
Postgres does not use a clustered primary key by default. Rows are stored in heap files (insertion order), and the primary key is a separate B-tree index. This means v4 random UUIDs do not cause the same catastrophic fragmentation as in MySQL.
However, random UUIDs still cause index bloat in Postgres over time. The B-tree index for a random UUID column will fragment faster than for an ordered column. For high-write tables, this is measurable.
For Postgres-specific UUID generation, you have choices:
-- Extension-based (older, but common)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SELECT uuid_generate_v4(); -- v4, random
-- Built-in (Postgres 13+, no extension needed)
SELECT gen_random_uuid(); -- v4, random
-- For v7 in Postgres, you need an extension or generate in app layer
-- The pg_uuidv7 extension provides it:
CREATE EXTENSION IF NOT EXISTS "pg_uuidv7";
SELECT uuid_generate_v7();The idiomatic recommendation for Postgres primary keys in 2026: generate v7 in your application layer and store it as a UUID column type. Postgres stores UUID as 16 bytes natively — far more efficient than a VARCHAR(36) string.
-- Native UUID type: 16 bytes
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL
);
-- VARCHAR(36): 36 bytes + overhead
-- Never do this
CREATE TABLE users (
id VARCHAR(36) PRIMARY KEY,
email TEXT NOT NULL
);MongoDB's ObjectId — When You Don't Need a UUID at All
MongoDB's native ObjectId is a 12-byte identifier that encodes a 4-byte timestamp, a 5-byte random value, and a 3-byte auto-incrementing counter. It is time-ordered by default, lightweight, and works extremely well with MongoDB's indexing.
If you are building on MongoDB, reach for ObjectId before reaching for a UUID. Only switch to UUID if you need interoperability with external systems that require RFC-compliant identifiers.
The Security Misconception That Keeps Getting Developers in Trouble
Here is the thing developers keep getting wrong: a UUID is not a secret. It is just hard to guess.
Those are very different security properties.
A v4 UUID has 122 bits of randomness. The chance of guessing a specific UUID is astronomically low. But "hard to guess" only protects you if the attacker is brute-forcing. It does not protect you if:
- Your API leaks all UUIDs through a list endpoint
- An attacker can enumerate records via timing side-channels
- Your logs expose the IDs and your logging pipeline is compromised
The correct framing: UUIDs prevent accidental collision and make sequential enumeration impossible. They are not access control. Do not confuse them for authentication tokens.
// This is wrong — treating a UUID as a bearer credential
const resourceUrl = `/api/reports/${reportId}`; // if reportId is exposed, anyone with it can access
// This is right — separate auth from identification
const resourceUrl = `/api/reports/${reportId}`;
// + Authorization: Bearer <jwt_token>If you need a secret token — password reset links, OAuth tokens, API keys — use a cryptographically random token of at least 256 bits generated by your platform's CSPRNG (crypto.randomBytes(32) in Node.js, secrets.token_urlsafe() in Python). Do not use a UUID for that purpose.
Language-Specific Implementations
Python
import uuid
# v4 — random
user_id = uuid.uuid4()
# v5 — deterministic from a URL
page_id = uuid.uuid5(uuid.NAMESPACE_URL, "https://example.com/page/1")
# v7 — not in stdlib yet; use the 'uuid6' library
# pip install uuid6
import uuid6
ordered_id = uuid6.uuid7()Node.js / TypeScript
// Native crypto (v4) — Node 14.17+
import { randomUUID } from "crypto";
const id = randomUUID();
// uuid package — supports v1, v3, v4, v5, v7
import { v4 as uuidv4, v7 as uuidv7 } from "uuid";
const randomId = uuidv4();
const orderedId = uuidv7(); // recommended for DB primary keysGo
import "github.com/google/uuid"
// v4
id := uuid.New()
// v7
v7id, err := uuid.NewV7()Java
import java.util.UUID;
// v4 — the only version in the standard library
UUID id = UUID.randomUUID();
// For v7, use the java-uuid-generator library:
import com.fasterxml.uuid.Generators;
UUID v7id = Generators.timeBasedEpochGenerator().generate();Alternatives Worth Knowing
UUID isn't the only option, and for many use cases it's not even the best one.
| Identifier | Size | Ordered? | URL-safe? | Best For |
|---|---|---|---|---|
| UUID v4 | 16 bytes / 36 chars | ❌ | ❌ (with hyphens) | Reference tokens, non-PK usage |
| UUID v7 | 16 bytes / 36 chars | ✅ | ❌ (with hyphens) | DB primary keys with time ordering |
| ULID | 16 bytes / 26 chars | ✅ | ✅ | Public-facing IDs, URLs |
| NanoID | configurable / 21 chars | ❌ | ✅ | Short, URL-safe unique IDs |
| Snowflake | 8 bytes / 18 chars | ✅ | ✅ | Distributed systems, Twitter/Discord scale |
| CUID2 | Variable / ~24 chars | ❌ | ✅ | Modern web, replaces CUID |
ULID is worth calling out specifically. It is UUID-compatible (128 bits), encodes a timestamp for natural ordering, and uses Crockford Base32 encoding — which means it is URL-safe and 10 characters shorter than a hyphenated UUID. If you need an ordered, public-facing identifier, ULID is often the right call.
// ULID in Node.js
import { ulid } from "ulid";
const id = ulid(); // "01ARYZ6S41TSV4RRFFQ69G5FAV"NanoID is better when size matters and you don't need time ordering. Configurable length and alphabet — great for short links, codes, and non-database identifiers.
import { nanoid } from "nanoid";
const id = nanoid(); // "V1StGXR8_Z5jdHi6B-myT"
const shortId = nanoid(10); // "IRFa-VaY2b"My Production Recommendations
To make this concrete:
Database primary keys: Use UUID v7, or integer sequences if you do not need global uniqueness. Avoid v4 on MySQL. Postgres is more forgiving but v7 is still preferable.
Public-facing IDs (URLs, API responses): UUID v7 or ULID. Both are ordered and URL-safe with minor formatting tweaks.
Deterministic IDs from known data: UUID v5. Underused, genuinely useful.
Short, one-time codes (invites, links, tokens): NanoID. Not UUIDs.
Security tokens (auth, API keys, password reset): crypto.randomBytes(32) encoded as base64url or hex. Not UUIDs.
Never expose as a secret: Any UUID version.
The mental model I come back to is simple: UUIDs guarantee uniqueness, not secrecy. And among UUID versions, the only meaningful question is whether you need your IDs to be ordered. If you do — and database primary keys almost always benefit from it — v7 is the answer. Everything else is context.
If you are still defaulting to v4 everywhere, you are not wrong. But you might be leaving performance on the table without knowing it.