If you've ever needed a unique ID without asking a database, UUIDs are your answer. No coordination, no auto-increment sequence, no "let me check if this ID exists first" — just generate one locally and move on. I've used them in everything from distributed microservices to quick prototypes where I didn't want to set up a database yet. They're one of those unglamorous tools that quietly makes distributed systems possible, and once you understand the different versions, you'll know exactly when to reach for them.
What Is a UUID?
A UUID (Universally Unique Identifier) is a 128-bit label used to uniquely identify information in computer systems. It is standardized in RFC 4122 and represented as a 32-character hexadecimal string split into five groups by hyphens:
550e8400-e29b-41d4-a716-446655440000The probability of generating two identical UUIDs is astronomically small — for UUID v4, you would need to generate approximately 2.7 quintillion UUIDs before having a 50% chance of a collision. In practice, UUIDs are treated as globally unique.
UUID Versions Explained
Generated from the current timestamp and the MAC address of the machine. Guaranteed unique across time and space, but exposes the machine's MAC address and generation time — a privacy concern.
Best for: Legacy systems, when you need sortable IDs by creation time.
Generated by hashing a namespace and a name using MD5. The same namespace + name always produces the same UUID. Deterministic but uses the outdated MD5 algorithm.
Best for: When you need a consistent ID for the same input (e.g., URL-based IDs). Prefer v5.
Generated from cryptographically secure random numbers. No information about the machine or time is embedded. The most widely used version for general-purpose unique IDs.
Best for: Database primary keys, session IDs, API keys, file names — most use cases.
Like v3 but uses SHA-1 instead of MD5. Deterministic — the same namespace + name always produces the same UUID. More secure than v3.
Best for: When you need reproducible IDs from known inputs (e.g., generating a consistent ID for a URL or email address).
A newer standard that combines a Unix timestamp prefix with random bits. Produces sortable UUIDs that are also random — the best of v1 and v4.
Best for: Modern database primary keys where you want both uniqueness and chronological ordering.
UUID vs Auto-Increment IDs
UUID Advantages
- Can be generated client-side without a database
- Safe to expose in URLs (no sequential enumeration)
- Works across distributed systems and databases
- No coordination needed between services
Auto-Increment Advantages
- Smaller storage (4–8 bytes vs 16 bytes)
- Naturally sorted by creation order
- Faster index performance in some databases
- Easier to read and remember
UUID Implementation Across Languages and Databases
Generating UUIDs is straightforward in every major language, but storing and indexing them efficiently in databases requires some thought. This section covers the practical implementation details across the most common environments.
JavaScript: crypto.randomUUID()
Modern JavaScript (Node.js 14.17+ and all modern browsers) includes a built-in UUID v4 generator in the Web Crypto API. No external library needed:
// Browser and Node.js 14.17+
const id = crypto.randomUUID()
// → "550e8400-e29b-41d4-a716-446655440000"
// For older Node.js versions, use the uuid package
const { v4: uuidv4, v7: uuidv7 } = require("uuid")
const id = uuidv4() // Random UUID v4
const sortableId = uuidv7() // Time-ordered UUID v7Python: uuid Module
Python's standard library includes the uuid module with support for all major UUID versions:
import uuid # UUID v4 (random) — most common id = uuid.uuid4() print(str(id)) # → "550e8400-e29b-41d4-a716-446655440000" # UUID v5 (name-based, deterministic) namespace = uuid.NAMESPACE_URL id = uuid.uuid5(namespace, "https://example.com") # Same URL always produces the same UUID # UUID v1 (time-based) id = uuid.uuid1() # Access components print(id.hex) # Without hyphens print(id.bytes) # As 16 raw bytes
Java: UUID.randomUUID()
Java's java.util.UUID class provides UUID generation and parsing:
import java.util.UUID;
// Generate UUID v4
UUID id = UUID.randomUUID();
String idString = id.toString();
// → "550e8400-e29b-41d4-a716-446655440000"
// Parse a UUID string
UUID parsed = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
// Name-based UUID v5 (requires manual implementation or library)
// Use com.fasterxml.uuid:java-uuid-generator for v5 and v7 supportStoring UUIDs in PostgreSQL
PostgreSQL has a native UUID data type that stores UUIDs as 16 bytes internally (not as a 36-character string). This is the most efficient option and supports indexing, comparison, and the gen_random_uuid() function for server-side generation:
-- Create a table with UUID primary key
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Insert with server-generated UUID
INSERT INTO users (email) VALUES ('alice@example.com');
-- Insert with client-provided UUID
INSERT INTO users (id, email) VALUES (
'550e8400-e29b-41d4-a716-446655440000',
'bob@example.com'
);Storing UUIDs in MySQL
MySQL does not have a native UUID type. You have two options: CHAR(36) stores the UUID as a human-readable string (36 characters with hyphens), while BINARY(16) stores the raw 16 bytes — half the storage and faster index operations.
-- CHAR(36) — readable but larger CREATE TABLE users ( id CHAR(36) PRIMARY KEY DEFAULT (UUID()), email VARCHAR(255) NOT NULL ); -- BINARY(16) — compact and fast CREATE TABLE users ( id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID(), 1)), email VARCHAR(255) NOT NULL ); -- The second argument to UUID_TO_BIN reorders bytes for better index locality
Performance Considerations for UUID Primary Keys
UUID v4 primary keys have a known performance issue with B-tree indexes: because UUIDs are random, each new insert goes to a random position in the index, causing frequent page splits and poor cache locality. This can significantly degrade write performance at scale.
The solutions are: use UUID v7 (time-ordered) which inserts at the end of the index like auto-increment IDs, use ULID (Universally Unique Lexicographically Sortable Identifier) as an alternative, or in MySQL use UUID_TO_BIN(uuid, 1) which reorders the time bits for sequential insertion. For most applications with under a few million rows, the performance difference is negligible.
Generate UUIDs Online
🆔 UUID Generator
Generate UUID v4 instantly in your browser — no server, no tracking
UUID Best Practices in Database Design
UUIDs are a natural fit for database primary keys, but using them effectively requires understanding their impact on storage, indexing, and query performance. The wrong approach can lead to bloated indexes and degraded write throughput at scale.
Indexed UUID Columns
B-tree indexes work best with sequential data. Random UUID v4 values cause index page splits because each new row inserts at a random position in the tree. For tables with millions of rows and heavy write traffic, this can reduce insert throughput by 30–50% compared to sequential keys. The fix is to use UUID v7 (time-ordered) or store the time-based prefix in the most significant bits so inserts are append-only.
UUID vs Auto-Increment Performance
Auto-increment IDs are 4–8 bytes and always insert at the end of the index. UUIDs are 16 bytes (as binary) or 36 bytes (as string) and insert randomly. For read-heavy workloads, the performance difference is negligible. For write-heavy workloads with large tables, auto-increment keys have better insert performance due to sequential access patterns. However, UUIDs eliminate the need for a central sequence generator — critical in distributed systems, microservices, and offline-first applications where multiple nodes create records simultaneously.
Storing UUIDs: BINARY(16) vs VARCHAR(36)
In MySQL and similar databases without a native UUID type, you have two storage options. VARCHAR(36) stores the human-readable hyphenated form and is easy to debug but uses more than twice the space. BINARY(16) stores the raw bytes — it is compact, faster to index, and should be preferred for production use. Use application-level conversion functions or database helpers likeUUID_TO_BIN() and BIN_TO_UUID() in MySQL 8+.
-- Storage comparison for 1 million rows: -- VARCHAR(36): ~36 MB for the column alone -- BINARY(16): ~16 MB for the column alone -- INT (auto-increment): ~4 MB -- Recommended: BINARY(16) with swap flag for time-ordering CREATE TABLE orders ( id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID(), 1)), customer_id BINARY(16) NOT NULL, total DECIMAL(10,2), INDEX idx_customer (customer_id) );
UUIDs in Distributed Systems
UUIDs shine in distributed architectures where multiple services or database nodes need to create records independently without coordination. Each service generates its own IDs locally with zero network overhead and zero risk of collision. This eliminates single points of failure and allows offline record creation that syncs later. Event sourcing, CQRS architectures, and multi-region databases all benefit from UUID-based identity.
When NOT to Use UUIDs
UUIDs are not always the right choice. Avoid them when storage space is critical (IoT, embedded systems), when you need human-readable sequential IDs (order numbers, invoice IDs), when the table is append-only with a single writer (auto-increment is simpler and faster), or when you need IDs that encode business meaning. For public-facing identifiers that need to be short and memorable, consider alternatives like NanoID, ULID, or custom short-code generators.
Generating UUIDs in Different Languages
Every major programming language provides built-in or standard-library support for generating UUIDs. Here are quick, copy-paste examples for the most common environments.
JavaScript
// Built-in (Node.js 14.17+, all modern browsers)
const id = crypto.randomUUID()
// → "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
// Using the uuid package (supports v1, v4, v5, v7)
import { v4 as uuidv4, v7 as uuidv7 } from "uuid"
const randomId = uuidv4()
const sortableId = uuidv7()Python
import uuid # UUID v4 (random) — most common my_id = uuid.uuid4() print(str(my_id)) # → "550e8400-e29b-41d4-a716-446655440000" # UUID v5 (deterministic from namespace + name) my_id = uuid.uuid5(uuid.NAMESPACE_URL, "https://example.com")
Java
import java.util.UUID;
// UUID v4 (random)
UUID id = UUID.randomUUID();
String idStr = id.toString();
// → "550e8400-e29b-41d4-a716-446655440000"
// Parse from string
UUID parsed = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");Go
import "github.com/google/uuid"
// UUID v4 (random)
id := uuid.New()
fmt.Println(id.String())
// → "550e8400-e29b-41d4-a716-446655440000"
// UUID v7 (time-ordered)
id, _ = uuid.NewV7()
// Parse from string
parsed, err := uuid.Parse("550e8400-e29b-41d4-a716-446655440000")Command Line
# Linux / macOS
uuidgen
# → 550E8400-E29B-41D4-A716-446655440000
# Using Python one-liner
python3 -c "import uuid; print(uuid.uuid4())"
# Using Node.js one-liner
node -e "console.log(crypto.randomUUID())"
# Generate multiple UUIDs
for i in {1..5}; do uuidgen; doneRegardless of language, always use cryptographically secure random number generators for UUID v4. Never use Math.random() or similar non-cryptographic sources — they do not provide sufficient entropy and can produce collisions in high-volume systems.
UUID as Database Primary Keys — The Real Trade-offs
This is where UUID conversations get spicy. Half the internet will tell you UUIDs are terrible primary keys, the other half uses them everywhere. The truth, as always, is "it depends." Here's what actually matters:
The B-Tree Index Problem
UUID v4 is completely random. When you insert random values into a B-tree index (which is what PostgreSQL, MySQL, and most databases use), the new value can land anywhere in the tree. This causes "page splits" — the database has to reorganize index pages constantly. With sequential IDs (auto-increment or UUID v7), new values always go at the end, which is much more efficient.
-- PostgreSQL: UUID v4 as primary key CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); -- MySQL: Use BINARY(16) for storage efficiency (saves 50% vs VARCHAR(36)) CREATE TABLE orders ( id BINARY(16) PRIMARY KEY, user_id BINARY(16) NOT NULL, total DECIMAL(10,2), INDEX idx_user (user_id) ); -- Insert with UUID_TO_BIN (MySQL 8.0+ — reorders bytes for better index performance) INSERT INTO orders (id, user_id, total) VALUES (UUID_TO_BIN(UUID(), true), UUID_TO_BIN(?, true), 99.99); -- PostgreSQL: UUID v7 for time-ordered inserts (best of both worlds) -- Requires pg_uuidv7 extension or application-side generation CREATE EXTENSION IF NOT EXISTS pg_uuidv7; CREATE TABLE events ( id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), type TEXT NOT NULL, payload JSONB );
Storage Size Comparison
| ID Type | Storage | Index Perf | Sortable | Best For |
|---|---|---|---|---|
| BIGINT (auto-increment) | 8 bytes | Excellent | ✅ Yes | Internal systems, high write volume |
| UUID v4 (binary) | 16 bytes | Poor (random) | ❌ No | Distributed systems, public APIs |
| UUID v7 (binary) | 16 bytes | Good (time-ordered) | ✅ Yes | Modern apps needing both benefits |
| UUID v4 (varchar) | 36 bytes | Poor | ❌ No | Avoid — wastes 2x storage |
| ULID | 16 bytes | Good (time-ordered) | ✅ Yes | Alternative to UUID v7 |
| Snowflake ID | 8 bytes | Excellent | ✅ Yes | Twitter-scale distributed systems |
My recommendation for most apps in 2026: use UUID v7 stored as BINARY(16). You get time-ordering (great index performance), global uniqueness (no coordination), and 128 bits of entropy. If your ORM doesn't support UUID v7 natively yet, generate it in application code and pass it as binary.
UUID Alternatives: ULID, NanoID, CUID, Snowflake
UUID isn't the only game in town. Several alternatives solve specific problems that UUID v4 doesn't handle well. Here's when you'd consider each:
ULID (Universally Unique Lexicographically Sortable Identifier)
26 characters, time-ordered, URL-safe. Same 128 bits as UUID but encoded as Crockford Base32 (no hyphens). Sortable by creation time.
01ARZ3NDEKTSV4RRFFQ69G5FAVNanoID
Configurable length (default 21 chars), URL-safe alphabet, smaller than UUID for URLs. Great for short public-facing IDs.
V1StGXR8_Z5jdHi6B-myTCUID2
Collision-resistant, horizontally scalable, secure. Designed specifically for web apps. No timestamp leakage.
clh3am10x000008mh35kd7e5rSnowflake ID (Twitter/Discord style)
64-bit integer: 41 bits timestamp + 10 bits worker ID + 12 bits sequence. Sorted, compact, but requires coordination (worker ID assignment).
175928847299117063Common UUID Mistakes I've Seen in Production
⚠️ Storing UUID as VARCHAR(36) instead of BINARY(16)
Impact: Uses 2.25x more storage, slower joins and index lookups, more memory pressure on large tables.
Fix: Use native UUID type (PostgreSQL) or BINARY(16) (MySQL). Convert at application boundary.
⚠️ Using UUID v4 as a primary key in MySQL InnoDB
Impact: Random inserts cause B-tree page splits, fragmented indexes, 30-40% slower writes at scale.
Fix: Use UUID v7 (time-ordered) or MySQL 8's UUID_TO_BIN(uuid, true) which reorders timestamp bytes for sequential index writes.
⚠️ Treating UUIDs as secret tokens
Impact: UUID v1 leaks MAC address and creation time. UUID v4 is random but not a replacement for cryptographic tokens.
Fix: For auth tokens, use crypto.randomBytes(32).toString('hex') or similar CSPRNG output. UUIDs are identifiers, not secrets.
⚠️ Comparing UUIDs as strings (case-sensitive)
Impact: 550e8400... and 550E8400... are the same UUID but fail string equality in many languages.
Fix: Parse to native UUID type before comparison, or normalize to lowercase. PostgreSQL's UUID type handles this automatically.
⚠️ Generating UUID v4 with Math.random()
Impact: Math.random() isn't cryptographically secure. In high-volume systems, you'll get collisions. In security contexts, they're predictable.
Fix: Always use crypto.randomUUID() (browser/Node.js 19+), uuid package, or your language's CSPRNG.
Summary
My advice? UUID v4 for most things. It's random, it's simple, every language has a one-liner to generate it. If you need your IDs to sort chronologically (and you probably do if you're using them as database primary keys), go with UUID v7 — it's time-ordered so your indexes stay happy. Don't overthink it beyond that. I've seen teams spend days debating UUID versions when any of them would have worked fine.
