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:

VersionStrategyStandardizedStill Relevant?
v1Timestamp + MAC addressRFC 4122 (2005)Mostly replaced by v7
v4RandomRFC 4122 (2005)Yes — the current default
v5Name-based (SHA-1 hash)RFC 4122 (2005)Niche use cases
v7Timestamp + randomRFC 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

PropertyUUID v4UUID v7
Format128-bit, 36-char hex128-bit, 36-char hex
Random bits122~74
Sortable by creation timeNoYes (ms precision)
B-tree index performancePoor at scaleExcellent
Timestamp leakageNoneYes (ms precision)
RFC standardRFC 4122 / RFC 9562RFC 9562 (May 2024)
Database native supportAll major databasesPostgreSQL 18+, growing
Library supportUniversalBroad and growing
Storage size16 bytes (binary)16 bytes (binary)
Column type compatibilityuuid / 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:

PropertyUUID v7ULID
Encoding36-char hex with hyphens26-char Crockford Base32
Timestamp48-bit ms epoch48-bit ms epoch
Random bits~7480
StandardIETF RFC 9562Community spec (no RFC)
Monotonic orderingImplementation-dependentSpec-defined increment within same ms
Database typeNative uuid columnRequires CHAR(26) or BINARY(16)
URL-safeNo (hyphens)Yes (Base32)
Timestamp valid untilYear 10889Year 10889

Where UUID v7 Wins

  1. 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.

  2. Native database support. ULIDs don’t fit in PostgreSQL’s native uuid column without conversion. You either store them as strings (wasting space) or convert to binary (losing the Crockford encoding). UUID v7 drops directly into existing uuid columns.

  3. 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

  1. 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.

  2. 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.

  3. 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

MetricUUID v4UUID v7 / Sequential
INSERT throughput (large dataset)20–30% baseline100% baseline
WAL volume~20 GB~2.5 GB
Cache hit ratio85%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

  1. Update your ID generation — switch application-level UUID generation to v7. For PostgreSQL 18+, change the column default to uuidv7().
  2. Update ORMs — most ORMs delegate UUID generation to the application or database. Update the generator, not the column type.
  3. Monitor index performance — after switching, new inserts will be sequential. Over time, your indexes will naturally become more efficient as v7 rows dominate.
  4. 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_at column
  • 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.