UUID vs Auto-Increment vs ULID: Which ID Generator Should You Use for Your Database?
Published: July 6, 2025 · 7 min read
Every database table needs a primary key — but how you generate that key has consequences that ripple through your entire application architecture. Should you let the database auto-increment? Should you generate UUIDs in your application code? What about ULIDs? The right answer depends on factors most tutorials never mention: your deployment topology, your index performance, and whether your IDs will ever be exposed to users. Let's compare the three major strategies side by side.
Strategy 1: Auto-Increment IDs
Auto-increment is the default in most SQL databases (MySQL's AUTO_INCREMENT, PostgreSQL's SERIAL or IDENTITY). Every new row gets the next sequential integer: 1, 2, 3, 4, and so on.
Pros
- Simple. Zero application code needed — the database handles it. You insert a row and get an ID back. No libraries, no generators, no thinking.
- Compact. A 4-byte integer (up to ~2.1 billion rows) or 8-byte bigint (up to ~9 quintillion). Indexes stay small and fast.
- Sortable by insertion order. Since IDs increase monotonically,
ORDER BY id DESCgives you the most recent rows. B-tree indexes work beautifully with sequential inserts — new rows always go to the rightmost leaf page. - Human-readable. "Order #48291" is easy to read, speak over the phone, or type into a search box.
Cons
- Predictable. If your API exposes
/users/48291, an attacker can trivially enumerate all your users by incrementing the number. This is the biggest practical concern for public-facing applications. - Single-point coordination. In a distributed system with multiple database servers, you can't independently generate IDs on different nodes without risking collisions. Solutions like distributed sequences exist but add complexity.
- Data leakage. Competitors can estimate your growth rate by signing up twice, a month apart, and comparing the IDs they received. "They added 50,000 users this month" is now public information.
- Merge conflicts. If you ever need to merge data from two databases, their auto-increment counters will overlap, requiring painful ID remapping.
Best for:
Single-database applications where IDs are never exposed to users, internal tools, and tables where compact indexes matter (logs, analytics, high-volume event data).
Strategy 2: UUID v4 (Random)
UUID v4 generates a 128-bit (36-character) identifier from random numbers, producing values like 550e8400-e29b-41d4-a716-446655440000. The probability of collision is so low it's effectively zero — you'd need to generate 1 billion UUIDs per second for 85 years to have a 50% chance of a single collision.
Pros
- Globally unique without coordination. Any server, any process, anywhere in the world can generate a UUID independently with confidence it won't collide with any other. This is the killer feature for distributed systems and microservices.
- Not enumerable. UUIDs reveal no information about record count, creation order, or insertion rate. Much safer for public APIs.
- Can be generated client-side. Your frontend or mobile app can generate an ID before even sending a request to the server, enabling optimistic UI updates and offline-first architectures.
- Universally supported. Every language and database has UUID support built in or available via standard libraries.
Cons
- Large. 128 bits (16 bytes) vs. 4-8 bytes for integers. In a table with 100 million rows, that's an extra 800 MB to 1.2 GB just for the primary key. Every foreign key referencing that table also balloons in size.
- Not sortable. Random UUIDs scatter across B-tree index pages randomly. This causes page splits, cache misses, and write amplification. On a busy table, random UUIDs can degrade insert performance by 30-50% compared to sequential IDs.
- Not human-friendly. Nobody wants to read a UUID over the phone: "That's order number 5-5-0-e-8-4-0-0..."
Best for:
Distributed systems, microservices, multi-tenant SaaS, public-facing APIs where enumeration is a concern, and any scenario where you need IDs generated before database insertion.
Strategy 3: ULID (Universally Unique Lexicographically Sortable Identifier)
ULID is the newcomer that tries to give you the best of both worlds. A ULID like 01ARZ3NDEKTSV4RRFFQ69G5FAV is 26 characters and consists of two parts: a 48-bit timestamp (first 10 characters) followed by 80 bits of randomness (last 16 characters).
Pros
- Sortable by creation time. The timestamp prefix means ULIDs are lexicographically sortable.
ORDER BY idworks correctly without a separatecreated_atcolumn. This makes B-tree indexes happy — inserts are mostly sequential. - URL-safe. ULID uses Crockford's base32 (uppercase letters and digits only — no dashes, no special characters). It's safe to use in URLs without encoding.
- More compact than UUID when stored as text. 26 characters vs. 36 for a string UUID. As binary, both are 16 bytes (128 bits), but ULID's string representation is shorter.
- Case-insensitive. ULID is defined as uppercase only, eliminating a common source of bugs where systems disagree on UUID casing.
Cons
- Less universal support. Major databases don't have native ULID types. You'll need a library in your application layer, though libraries exist for all major languages.
- Timestamp leakage. The first 10 characters encode the creation time. While less revealing than sequential integers, a determined observer can still determine relative creation order.
- Smaller ecosystem. Fewer tools, extensions, and ORM integrations compared to UUID's decades of universal adoption.
Best for:
Modern distributed applications where you want sortable, URL-safe IDs without giving up decentralization. Particularly good for event sourcing, append-only logs, and systems that use ID-based pagination.
Side-by-Side Comparison
| Feature | Auto-Increment | UUID v4 | ULID |
|---|---|---|---|
| Storage size | 4-8 bytes | 16 bytes | 16 bytes |
| Sortable | ✓ Yes | ✗ No | ✓ Yes (time) |
| Distributed-friendly | ✗ No | ✓ Yes | ✓ Yes |
| Non-enumerable | ✗ No | ✓ Yes | ~ Partial |
| Human-readable | ✓ Excellent | ✗ Poor | ~ Moderate |
| URL-safe | ✓ Yes | ✓ Yes | ✓ Yes |
| Index performance | ✓ Optimal | ✗ Degraded | ✓ Good |
| Native DB support | ✓ Universal | ✓ Universal | ✗ Library needed |
Which One Should You Choose?
Use Auto-Increment when:
- You have a single database instance (or read replicas with a single write master)
- IDs are never exposed in URLs or API responses (or you use a separate public slug)
- Storage efficiency and index performance are critical (high-volume logging, time-series data)
- You're building internal tools, admin panels, or data warehouses
Use UUID v4 when:
- You're building a distributed system with multiple write nodes
- IDs will be publicly visible and enumeration must be prevented
- You need client-side ID generation (offline-first apps, mobile apps with spotty connectivity)
- You're working with microservices that need to generate IDs independently
Use ULID when:
- You want the distribution benefits of UUID but need sortable IDs for chronological queries
- You're building an event-sourced system or append-only log
- Your application uses ID-based cursor pagination (like Twitter's API)
- URL-friendliness matters and you'd rather not deal with UUID dashes
The Hybrid Approach: Internal vs. Public IDs
Many experienced teams use both: an auto-increment integer as the internal primary key (for fast joins and compact foreign keys) and a UUID or ULID as the public-facing identifier exposed in APIs and URLs. This gives you the best of both worlds — efficient database internals with safe, non-enumerable external identifiers. The cost is one extra column per table and slightly more complex application logic. For most production applications serving external users, this tradeoff is almost always worth it.
If you need UUIDs, our free UUID Generator generates UUID v4 identifiers instantly — all generated locally in your browser with cryptographically secure randomness. Generate one or generate a batch; no server calls needed.