UUID v4 vs v7 vs ULID: How to Choose the Right Identifier for Your Database
You’re starting a new service, and the first decision is deceptively simple: what should your primary key look like? Auto-incrementing integers leak row counts, expose creation order, and break the moment you need to merge data across databases. UUIDs solve those problems — but now you have to choose which UUID.
UUID v4 has been the default for over a decade. UUID v7, standardized in RFC 9562 in May 2024, promises better database performance through time-ordering. And ULID, the community-driven alternative from 2016, offers similar benefits in a more compact format. Each has real trade-offs that affect performance, privacy, and long-term maintainability.
This guide compares all three — with real benchmark data, practical migration advice, and a clear decision framework for 2026.
What Is a UUID? (And Why “GUID” Is the Same Thing)
A UUID (Universally Unique Identifier) is a 128-bit value designed to be unique without a central authority. The standard format is 32 hexadecimal characters separated by hyphens: 550e8400-e29b-41d4-a716-446655440000.
If you’ve worked with .NET or Windows, you’ve seen these called GUIDs (Globally Unique Identifiers). They’re the same thing — GUID is Microsoft’s name for the UUID standard. The bit layout, generation algorithms, and storage requirements are identical.
UUID has gone through several versions. The ones that matter today:
| Version | Strategy | Standardized | Still Relevant? |
|---|---|---|---|
| v1 | Timestamp + MAC address | RFC 4122 (2005) | Mostly replaced by v7 |
| v4 | Random | RFC 4122 (2005) | Yes — the current default |
| v5 | Name-based (SHA-1 hash) | RFC 4122 (2005) | Niche use cases |
| v7 | Timestamp + random | RFC 9562 (2024) | Yes — the new recommendation |
RFC 9562 is explicit about direction: “Implementations SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6 if possible.”
UUID v4: The Random Standard
UUID v4 generates identifiers from 122 bits of cryptographically secure randomness. That’s a keyspace of approximately 5.3 × 10³⁶ possible values — you’d need to generate a billion UUIDs per second for 86 years to hit a 50% collision probability.
How it works: 128 bits total, with 6 bits reserved for version (4) and variant markers. The remaining 122 bits come from a CSPRNG like crypto.getRandomValues().
Example: f47ac10b-58cc-4372-a567-0e02b2c3d479
The simplicity is its strength. No coordination required, no timestamps to leak, no state to maintain. Every major language and database has v4 support built in.
The Problem: Random UUIDs Fragment Your Database
Pure randomness comes at a cost. When you insert random UUIDs into a B-tree index — the default for PostgreSQL, MySQL, and most relational databases — new rows land at arbitrary positions in the tree. This causes:
- Page splits: The database constantly splits and reorganizes index pages instead of appending to the end.
- Write amplification: EnterpriseDB benchmarks show random UUIDs generate 8× more WAL (write-ahead log) than sequential alternatives — 20GB vs 2.5GB for the same workload.
- Throughput collapse: On datasets larger than RAM, random UUID inserts drop to 20–30% of sequential UUID throughput.
- Wasted space: PlanetScale reports that random UUIDs cause MySQL’s B+ tree page utilization to fall to ~50%, compared to 94% with sequential keys.
This isn’t a theoretical concern. Buildkite saw a 50% reduction in WAL rate on their primary database after switching to time-ordered UUIDs. Shopify measured a 50% decrease in INSERT duration when moving from UUID v4 to time-ordered identifiers for payment system idempotency keys.
UUID v4 is fine for small tables, infrequent writes, or anywhere you don’t use the UUID as a primary key. For high-throughput systems, the fragmentation tax compounds over time.
UUID v7: Time-Ordered and Database-Friendly
UUID v7 solves the fragmentation problem by putting a timestamp first. The most significant 48 bits encode a UNIX epoch timestamp in milliseconds, followed by ~74 bits of cryptographically secure randomness.
How it works:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤
| unix_ts_ms (48 bits) |
├─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤
| unix_ts_ms | ver | rand_a (12 bits) |
├─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤
|var| rand_b (62 bits) |
├─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤
Because UUIDs generated in the same millisecond share a timestamp prefix, they cluster together in B-tree indexes. New inserts append near the end of the tree rather than scattering randomly — the same access pattern that makes auto-incrementing integers fast.
Example: 019078e5-d2c7-7b3a-8f1e-4a6d5c8b2e91 — the first 12 hex characters encode the creation time.
What RFC 9562 Changed
Before RFC 9562, time-ordered UUIDs existed only as informal proposals. The RFC, published May 2024, made UUID v7 an official IETF standard with a defined bit layout. It catalogues 16 different non-standard time-ordered ID implementations — including ULID, Twitter’s Snowflake, and Instagram’s ShardId — that motivated its creation.
The practical impact: library authors now have a single spec to implement against, and databases are adding native support. PostgreSQL 18 (released late 2025) shipped a built-in uuidv7() function, eliminating the need for extensions or application-level generation.
PostgreSQL 18’s Native uuidv7()
PostgreSQL 18, released in late 2025, adds two new functions:
-- Generate a time-ordered UUID v7
SELECT uuidv7();
-- Result: 019078e5-d2c7-7b3a-8f1e-4a6d5c8b2e91
-- Generate a random UUID v4 (alias for gen_random_uuid())
SELECT uuidv4();
PostgreSQL’s implementation adds a 12-bit sub-millisecond timestamp fraction, giving better monotonic ordering within a single backend process. This means even rows inserted in the same millisecond maintain creation order — important for high-throughput tables.
For earlier PostgreSQL versions, you can generate UUID v7 at the application level or use extensions like pg_idkit.
UUID v4 vs v7: Head-to-Head Comparison
| Property | UUID v4 | UUID v7 |
|---|---|---|
| Format | 128-bit, 36-char hex | 128-bit, 36-char hex |
| Random bits | 122 | ~74 |
| Sortable by creation time | No | Yes (ms precision) |
| B-tree index performance | Poor at scale | Excellent |
| Timestamp leakage | None | Yes (ms precision) |
| RFC standard | RFC 4122 / RFC 9562 | RFC 9562 (May 2024) |
| Database native support | All major databases | PostgreSQL 18+, growing |
| Library support | Universal | Broad and growing |
| Storage size | 16 bytes (binary) | 16 bytes (binary) |
| Column type compatibility | uuid / BINARY(16) | uuid / BINARY(16) — same column, fully compatible |
The critical insight: v4 and v7 are column-compatible. They use the same uuid data type, same 16-byte binary storage, same 36-character string representation. You can store both in the same column, which makes incremental migration straightforward.
Try it yourself: UUID Generator — generate UUID v4 identifiers instantly in your browser, with bulk generation and one-click copy.
UUID v7 vs ULID: Do You Still Need ULID?
ULID (Universally Unique Lexicographically Sortable Identifier) launched in 2016 to solve the same problem UUID v7 now addresses: time-ordered, globally unique identifiers. Here’s how they compare:
| Property | UUID v7 | ULID |
|---|---|---|
| Encoding | 36-char hex with hyphens | 26-char Crockford Base32 |
| Timestamp | 48-bit ms epoch | 48-bit ms epoch |
| Random bits | ~74 | 80 |
| Standard | IETF RFC 9562 | Community spec (no RFC) |
| Monotonic ordering | Implementation-dependent | Spec-defined increment within same ms |
| Database type | Native uuid column | Requires CHAR(26) or BINARY(16) |
| URL-safe | No (hyphens) | Yes (Base32) |
| Timestamp valid until | Year 10889 | Year 10889 |
Where UUID v7 Wins
-
Standardization. UUID v7 has an IETF RFC. Every database, ORM, and language runtime already understands the UUID type. ULID requires custom parsing in most ecosystems.
-
Native database support. ULIDs don’t fit in PostgreSQL’s native
uuidcolumn without conversion. You either store them as strings (wasting space) or convert to binary (losing the Crockford encoding). UUID v7 drops directly into existinguuidcolumns. -
Ecosystem momentum. Bytebase predicts the industry will “gradually abandon bespoke solutions and converge on UUIDv7 as the primary key for most use cases.” PostgreSQL 18’s native
uuidv7()accelerates this.
Where ULID Still Makes Sense
-
Compact representation. At 26 characters vs 36, ULIDs are shorter in URLs and APIs. If string length matters for your use case, ULID is more compact.
-
Slightly more randomness. ULID provides 80 random bits vs UUID v7’s ~74. In practice, both have astronomically low collision rates, but ULID has a marginally larger random component.
-
Existing ULID infrastructure. If your system already uses ULIDs and migration cost is non-trivial, there’s no urgent reason to switch. Both provide comparable database performance since they share the same 48-bit timestamp prefix.
For new projects in 2026, UUID v7 is the safer choice. It’s standardized, natively supported by databases, and doesn’t require your team to maintain ULID parsing logic.
Database Performance: Real Benchmarks
The performance difference between random and time-ordered identifiers is well-documented across production systems:
PostgreSQL
| Metric | UUID v4 | UUID v7 / Sequential |
|---|---|---|
| INSERT throughput (large dataset) | 20–30% baseline | 100% baseline |
| WAL volume | ~20 GB | ~2.5 GB |
| Cache hit ratio | 85% | 99% |
Source: EnterpriseDB benchmark. The gap widens as datasets exceed available RAM.
MySQL / InnoDB
MySQL’s InnoDB engine uses clustered indexes, where the primary key determines physical row order. Random UUIDs are especially costly:
- B+ tree page utilization drops to ~50% (vs 94% sequential)
- UUID stored as
CHAR(36)is 9× larger than a 32-bit integer BINARY(16)is the recommended storage format — 4× larger than an integer, but compact enough for most workloads
Source: PlanetScale analysis.
When to Keep Auto-Increment
UUIDs aren’t always the answer. Auto-incrementing integers remain the right choice when:
- You operate a single database with no sharding plans
- Write throughput matters more than global uniqueness
- Row counts are not sensitive information
- You don’t need to generate IDs outside the database
For distributed systems, microservices, or any architecture where IDs must be generated at the application level, UUID v7 gives you both the performance of sequential keys and the flexibility of decentralized generation.
How to Generate UUID v7
JavaScript / TypeScript
// Using the 'uuid' package (v10+)
import { v7 as uuidv7 } from 'uuid';
const id = uuidv7(); // '019078e5-d2c7-7b3a-8f1e-4a6d5c8b2e91'
Python
# Python 3.x with uuid7 package
from uuid_extensions import uuid7
id = str(uuid7()) # '019078e5-d2c7-7b3a-8f1e-4a6d5c8b2e91'
Go
// Using github.com/gofrs/uuid/v5
import "github.com/gofrs/uuid/v5"
id, _ := uuid.NewV7()
fmt.Println(id.String()) // "019078e5-d2c7-7b3a-8f1e-4a6d5c8b2e91"
PostgreSQL 18+
-- Native function, no extension needed
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
customer_id uuid NOT NULL,
created_at timestamptz DEFAULT now()
);
Other Languages
UUID v7 libraries are available for Rust (uuid crate v1.4+), Java (uuid-creator), C# (UUIDNext), and PHP (symfony/uid).
Migrating from UUID v4 to v7
Already running UUID v4 in production? The good news: migration doesn’t require a big-bang switchover.
v4 and v7 Can Coexist
Both versions share the same uuid column type and 16-byte binary representation. You can start generating v7 for new rows while existing v4 rows remain untouched. Queries, joins, and indexes work identically — the database doesn’t care which version a UUID is.
The only behavioral difference: new v7 rows will sort after older v4 rows when ordered by primary key, since v7’s timestamp prefix places them later in lexicographic order. If your application relies on UUID ordering (which it shouldn’t with v4), verify this assumption.
Migration Checklist
- Update your ID generation — switch application-level UUID generation to v7. For PostgreSQL 18+, change the column default to
uuidv7(). - Update ORMs — most ORMs delegate UUID generation to the application or database. Update the generator, not the column type.
- Monitor index performance — after switching, new inserts will be sequential. Over time, your indexes will naturally become more efficient as v7 rows dominate.
- Don’t backfill — converting existing v4 values to v7 is unnecessary and would break foreign key references. Let v4 and v7 coexist.
Privacy Consideration: Timestamps in Your IDs
UUID v7 encodes creation time with millisecond precision. Anyone who can see the UUID can extract when it was created. For internal database keys, this is rarely a concern. But consider the implications for:
- Public-facing APIs — if your API exposes entity IDs, clients can determine when resources were created. This might reveal business patterns (order volume, user growth rate).
- Session tokens and API keys — prefer UUID v4 for secrets. The timestamp in v7 adds no value for tokens and unnecessarily reveals timing information.
- Regulatory compliance — depending on your jurisdiction, creation timestamps embedded in identifiers might constitute personal data if they can be tied to a user.
A practical approach: use UUID v7 for database primary keys (internal), UUID v4 for externally-visible tokens and API keys.
Try it yourself: UUID Generator — need a batch of UUIDs for testing? Generate up to 25 at once, copy individually, or download as a text file.
Frequently Asked Questions
Can two UUIDs ever be the same?
Theoretically yes, but practically no. UUID v4’s 122 random bits give a keyspace of 5.3 × 10³⁶. You’d need to generate one billion UUIDs per second for 86 years to reach a 50% collision probability. UUID v7 has fewer random bits (~74), but collisions within the same millisecond are still astronomically unlikely.
Should I store UUIDs as strings or binary?
Binary. A UUID stored as CHAR(36) uses 36 bytes; as BINARY(16) it uses 16 bytes — less than half the storage. PostgreSQL’s native uuid type stores as 16-byte binary automatically. In MySQL, use BINARY(16) and convert at the application level. PlanetScale notes that CHAR(36) is 9× larger than a 32-bit integer, making binary storage essential for large tables.
How does UUID v7 compare to Twitter Snowflake?
Both are time-ordered distributed identifiers, but they serve different needs. Snowflake IDs are 64-bit (smaller, faster to compare), but require a central coordination service to assign worker IDs. UUID v7 is 128-bit and requires no coordination — any node can generate one independently. RFC 9562 explicitly lists Snowflake among the 16 non-standard implementations that motivated UUID v7’s creation.
How do I generate UUID v7 in PostgreSQL before version 18?
Before native support, you have several options: the pg_idkit extension, application-level generation (generate in your app code and pass to the database), or a PL/pgSQL function that assembles the timestamp and random bytes manually. Upgrading to PostgreSQL 18 is the cleanest path if your infrastructure allows it.
Is UUID v7 safe for API keys?
No — use UUID v4 for secrets. UUID v7’s timestamp reveals when the key was created, which is unnecessary information in a security context. For API keys and session tokens, you want maximum entropy with no embedded metadata. UUID v4’s 122 bits of pure randomness from a CSPRNG is appropriate, though dedicated token formats may offer additional features like built-in expiry.
Which Should You Choose? A Decision Framework
Choose UUID v4 when:
- You need maximum randomness (tokens, API keys, session IDs)
- Creation time must remain private
- You’re adding UUIDs to a small, low-write table where index fragmentation doesn’t matter
Choose UUID v7 when:
- The UUID serves as a database primary key
- You need time-based sorting without an extra
created_atcolumn - Write throughput matters at scale
- You want the benefits of sequential keys without a central authority
Choose ULID when:
- You need a shorter string representation (26 chars vs 36)
- Your system already runs on ULID infrastructure
- You need URL-safe identifiers without encoding
For most new projects in 2026, UUID v7 is the default recommendation. It combines the decentralized generation of UUID v4 with the database performance of sequential keys, backed by an IETF standard and growing native database support. The convergence toward v7 is well underway — PostgreSQL 18’s built-in uuidv7() function is the clearest signal yet.
Whatever you choose, you can generate UUID v4 identifiers instantly with the UUID Generator — or format your API responses containing UUIDs with the JSON Formatter.