Back to Thoughts

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:

SegmentBitsMeaning
550e840032Time-low (in v1) or random (in v4)
e29b16Time-mid
41d416Version encoded in the high nibble (M)
a71616Variant + clock sequence (N)
44665544000048Node (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 MAC

UUID 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 URL

When 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() # v4

It 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
  • SELECT queries 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 keys

Go

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.

IdentifierSizeOrdered?URL-safe?Best For
UUID v416 bytes / 36 chars❌ (with hyphens)Reference tokens, non-PK usage
UUID v716 bytes / 36 chars❌ (with hyphens)DB primary keys with time ordering
ULID16 bytes / 26 charsPublic-facing IDs, URLs
NanoIDconfigurable / 21 charsShort, URL-safe unique IDs
Snowflake8 bytes / 18 charsDistributed systems, Twitter/Discord scale
CUID2Variable / ~24 charsModern 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.


© 2026 Daniel Dallas Okoye

The best code is no code at all.