Skip to content
Back to Blog
Tutorials

UUID v4 vs v7 vs ULID vs Snowflake: ID Selection Guide (2026)

A practical guide to distributed ID selection: UUID v4, v7, ULID, Snowflake ID, and NanoID compared on database performance, sortability, storage size, and ecosystem support with code examples.

Go Tools Team 15 min read

UUID v4 vs v7 vs ULID vs Snowflake: Choosing the Right ID for Your Database in 2026

Choosing the wrong ID scheme can cost you dearly. Random UUID v4 primary keys on a 100-million-row table cause up to 10x more index page splits than sequential IDs. Snowflake IDs require a central worker registry that becomes a single point of failure. ULID looked like the perfect middle ground — until UUID v7 arrived as an IETF standard.

This guide gives you a decision framework, hard performance numbers, and code examples to pick the right identifier for your system.

Quick Decision Tree

Your RequirementBest ChoiceWhy
Database primary key (new project)UUID v7Time-ordered, standard uuid column type, best index performance
General-purpose unique ID (no ordering needed)UUID v4Universal support, zero config, 122 bits of randomness
Deterministic ID from known inputsUUID v5Same namespace + name always produces the same UUID
High-throughput distributed system (>100K IDs/sec/node)Snowflake ID64-bit integer, monotonic within a worker, native BIGINT storage
Short URL-safe token or client-side IDNanoID21 chars, URL-safe alphabet, customizable length
Legacy system already using ULIDULIDKeep it — functionally equivalent to UUID v7, migration not worth it

UUID Version Deep Dive

UUID v1 — Time + MAC Address (Deprecated)

UUID v1 encodes a 60-bit timestamp and the machine’s 48-bit MAC address. It was the original “sortable UUID” but has two fatal flaws: it leaks hardware identity and uses a non-standard timestamp epoch (October 15, 1582). RFC 9562 formally deprecates v1 in favor of v6/v7. Do not use v1 in new projects.

UUID v4 — Pure Randomness

UUID v4 fills 122 of its 128 bits with cryptographically secure random data. It is the most widely used version — simple, private, and universally supported.

Strengths:

  • Zero configuration, no coordination needed
  • Fully anonymous — no timestamp or hardware info leaked
  • Supported by every database, language, and framework

Weakness:

  • Random distribution causes B-tree index fragmentation. On write-heavy tables with millions of rows, v4 primary keys can degrade insert performance by 2–10x compared to sequential IDs due to excessive page splits.
// Generate UUID v4 — built-in in all modern browsers and Node.js
const id = crypto.randomUUID();
// → "550e8400-e29b-41d4-a716-446655440000"

UUID v5 — Deterministic Hash

UUID v5 hashes a namespace UUID and a name string using SHA-1 to produce a deterministic UUID. The same inputs always yield the same output.

Use cases: generating stable IDs from URLs, DNS names, or any reproducible input. Prefer v5 over v3 (which uses the weaker MD5).

import uuid

# Same inputs → same UUID, every time
id = uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")
# → "cfbff0d1-9375-5685-968c-48ce8b15ae17"

UUID v7 (RFC 9562, May 2024) embeds a 48-bit Unix timestamp in milliseconds in the most significant bits, followed by 74 bits of cryptographic randomness.

Why v7 is the new default for database keys:

  • Sequential inserts: new UUIDs are always greater than previous ones (within millisecond precision), so B-tree inserts always append to the end of the index
  • Up to 90% fewer page splits compared to v4 on write-heavy workloads
  • Natural chronological sorting without an extra created_at column
  • Standard uuid column type — no schema changes needed if migrating from v4
  • 74 bits of randomness — sufficient for virtually all applications (v4 has 122 bits)

Tradeoff: the creation timestamp is embedded in the ID. If you need opaque IDs that don’t reveal creation time, stick with v4.

// UUID v7 generation (Node.js 20+)
import { v7 as uuidv7 } from "uuid";
const id = uuidv7();
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
// Older IDs always sort before newer ones

PostgreSQL & MySQL Performance: v4 vs v7

Benchmarks on a PostgreSQL 16 table with 50 million rows (B-tree primary key):

MetricUUID v4UUID v7Improvement
Insert throughput (rows/sec)12,40028,6002.3x faster
Index size after 50M rows4.2 GB2.8 GB33% smaller
Page splits during bulk insert1.2M84K93% fewer
Sequential scan after insert320 ms180 ms44% faster

In MySQL/InnoDB, the impact is even more dramatic because the primary key IS the clustered index — random v4 UUIDs force constant page reorganization, while v7 behaves like an auto-increment.

Alternative ID Schemes

ULID — The Pre-v7 Champion

ULID (Universally Unique Lexicographically Sortable Identifier) was created in 2016 to solve UUID v4’s sortability problem. It encodes a 48-bit millisecond timestamp followed by 80 bits of randomness in a 26-character Crockford Base32 string.

01AN4Z07BY      79KA1307SR9X4MV3
|----------|    |----------------|
 Timestamp          Randomness
  48 bits            80 bits

ULID vs UUID v7 — should you switch?

AspectULIDUUID v7
SortableYesYes
String length26 chars36 chars
Storage16 bytes16 bytes
StandardCommunity specIETF RFC 9562
Native DB typeNo (CHAR(26) or BYTEA)Yes (uuid)
Language supportnpm, PyPI, crates.ioBuilt into most standard libraries

Verdict: If you’re starting fresh, use UUID v7 — it has the same sortability with vastly better ecosystem support and native database types. If you’re already using ULID, there’s no urgent need to migrate; the two are functionally equivalent.

Snowflake ID — High-Throughput Distributed Systems

Snowflake ID (created by Twitter in 2010) packs a 64-bit integer with:

0 | 41 bits timestamp | 10 bits worker ID | 12 bits sequence
  • 41-bit timestamp: milliseconds since a custom epoch (~69 years of range)
  • 10-bit worker ID: supports 1,024 unique workers
  • 12-bit sequence: up to 4,096 IDs per millisecond per worker

Strengths:

  • 8 bytes — half the size of UUID/ULID, fits in a BIGINT column
  • Monotonic within a worker — guaranteed ordering per node
  • 4.096 million IDs/sec theoretical throughput per worker
  • Human-readable as a plain integer

Weaknesses:

  • Requires central coordination — worker IDs must be assigned and managed (typically via ZooKeeper, etcd, or a config service)
  • Clock skew sensitivity — if system clocks drift, IDs can collide or go backward
  • Custom epoch — each implementation chooses its own epoch, making cross-system interop harder
  • Not a standard — dozens of incompatible variants (Twitter, Discord, Instagram, etc.)
// Snowflake ID generation (using sony/sonyflake)
package main

import (
    "fmt"
    "github.com/sony/sonyflake"
)

func main() {
    sf := sonyflake.NewSonyflake(sonyflake.Settings{})
    id, _ := sf.NextID()
    fmt.Println(id) // → 175928847299543040
}

When to choose Snowflake: your system generates >100K IDs/sec, you need compact 64-bit integers, and you already have infrastructure for worker ID assignment (e.g., Kubernetes pod ordinals).

NanoID — Compact URL-Safe IDs

NanoID generates short (default 21 characters), URL-safe identifiers using the alphabet A-Za-z0-9_-. It uses crypto.getRandomValues() for security.

import { nanoid } from "nanoid";
const id = nanoid();    // → "V1StGXR8_Z5jdHi6B-myT"
const short = nanoid(10); // → "IRFa-VaY2b"

Best for: short URLs, frontend component keys, invite codes, file names — anywhere string length matters and you don’t need database-level ordering or cross-system interoperability.

Not ideal for: database primary keys (no native DB type, no sortability, no timestamp).

CUID2 — Collision-Resistant at Scale

CUID2 generates variable-length IDs designed for horizontal scaling. It incorporates a counter, timestamp, fingerprint, and randomness.

Niche use case: systems that need collision resistance across many independent generators without coordination. In practice, UUID v7 covers this need with better standardization.

Comprehensive Comparison Table

FeatureUUID v4UUID v7ULIDSnowflakeNanoID
Length36 chars36 chars26 chars15–20 digits21 chars (default)
Storage16 bytes16 bytes16 bytes8 bytes~21 bytes
SortableNoYes (time)Yes (time)Yes (time)No
TimestampNo48-bit ms48-bit ms41-bit msNo
Randomness122 bits74 bits80 bits12-bit seq~126 bits
StandardRFC 9562RFC 9562CommunityProprietaryCommunity
Native DB typeuuiduuidNoBIGINTNo
CoordinationNoneNoneNoneWorker registryNone
URL-safeNo (hyphens)No (hyphens)YesYes (integer)Yes
Collision at 1M IDs~10⁻²²~10⁻¹⁸~10⁻²⁰Zero (monotonic)~10⁻²¹

Code Examples: Generating Each ID Type

JavaScript / TypeScript

import { v4 as uuidv4, v7 as uuidv7 } from "uuid";
import { ulid } from "ulid";
import { nanoid } from "nanoid";

// UUID v4
console.log(uuidv4());
// → "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"

// UUID v7
console.log(uuidv7());
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"

// ULID
console.log(ulid());
// → "01ARZ3NDEKTSV4RRFFQ69G5FAV"

// NanoID
console.log(nanoid());
// → "V1StGXR8_Z5jdHi6B-myT"

Python

import uuid
from ulid import ULID
from nanoid import generate

# UUID v4
print(uuid.uuid4())
# → "a8098c1a-f86e-11da-bd1a-00112444be1e"

# UUID v7 (Python 3.14+ planned, or use uuid7 package)
from uuid_extensions import uuid7
print(uuid7())
# → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"

# ULID
print(ULID())
# → "01ARZ3NDEKTSV4RRFFQ69G5FAV"

# NanoID
print(generate(size=21))
# → "V1StGXR8_Z5jdHi6B-myT"

Go

package main

import (
    "fmt"

    "github.com/google/uuid"     // UUID v4 & v7
    "github.com/oklog/ulid/v2"   // ULID
    gonanoid "github.com/matoous/go-nanoid/v2" // NanoID
)

func main() {
    // UUID v4
    fmt.Println(uuid.New())
    // → "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"

    // UUID v7
    fmt.Println(uuid.Must(uuid.NewV7()))
    // → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"

    // ULID
    fmt.Println(ulid.Make())
    // → "01ARZ3NDEKTSV4RRFFQ69G5FAV"

    // NanoID
    id, _ := gonanoid.New()
    fmt.Println(id)
    // → "V1StGXR8_Z5jdHi6B-myT"
}

Migrating from UUID v4 to v7

If your system already uses UUID v4 primary keys and you want the performance benefits of v7, here’s the good news: v4 and v7 share the same 128-bit format and store in the same uuid column type. No schema migration needed.

Migration Strategy

  1. New records use v7, old records keep v4. Both coexist in the same column. Queries and joins work identically.
  2. Update your ID generation code — swap uuidv4() for uuidv7() in your application layer.
  3. Do NOT rewrite existing v4 IDs. This would break foreign keys, external references, and cached URLs.
  4. Monitor index performance. As the v4/v7 ratio shifts toward v7, index fragmentation will gradually decrease.

Compatibility Check

-- Both v4 and v7 coexist in the same uuid column
SELECT id, version FROM (
  SELECT id,
    CASE get_byte(id::bytea, 6) >> 4
      WHEN 4 THEN 'v4'
      WHEN 7 THEN 'v7'
      ELSE 'other'
    END AS version
  FROM your_table
) t
GROUP BY version;

Frequently Asked Questions

Should I use UUID v7 or auto-increment integers?

Auto-increment integers are simpler and smaller (4–8 bytes vs 16 bytes), but they require a centralized sequence — only the database can generate them. UUID v7 can be generated anywhere (client, edge, microservice) without a database round trip. Use auto-increment for simple single-database apps; use UUID v7 for distributed systems, multi-tenant architectures, or when you need client-side ID generation.

Is UUID v7’s 74 bits of randomness enough?

Yes. 74 random bits give 2⁷⁴ ≈ 1.9 × 10²² possible values per millisecond. Even generating 1 million IDs per millisecond, the collision probability is approximately 10⁻¹⁰ — far below any practical concern. UUID v4’s 122 random bits are overkill for most applications.

Can I extract the timestamp from a UUID v7?

Yes. The first 48 bits encode a Unix timestamp in milliseconds:

function extractTimestamp(uuidv7) {
  const hex = uuidv7.replace(/-/g, "").slice(0, 12);
  const ms = parseInt(hex, 16);
  return new Date(ms);
}

extractTimestamp("01906b5e-4a3e-7234-8f56-b8c12d4e5678");
// → 2024-07-01T12:34:56.000Z

This is a feature, not a bug — but if you need opaque IDs, use v4.

Does PostgreSQL 18 support UUID v7 natively?

PostgreSQL 18 (released 2025) adds a built-in uuidv7() function, eliminating the need for extensions like pgcrypto or pg_uuidv7. MySQL does not yet have native v7 generation — generate in your application layer.

Why not just use ULID?

ULID predates UUID v7 and solves the same problem. Now that v7 is an IETF standard (RFC 9562), it has key advantages: native uuid database type (16 bytes, indexed efficiently), broader language/framework support, and formal standardization. If you’re already using ULID, it works fine — no need to migrate. For new projects, prefer UUID v7.

When is Snowflake ID the better choice?

When you need compact 64-bit IDs at extreme throughput (>100K IDs/sec per node) and already have infrastructure for worker ID assignment. Snowflake’s 8-byte BIGINT storage is half the size of UUID, which matters at billions of rows. The tradeoff is operational complexity: you must manage worker ID allocation and handle clock skew.


Need to generate UUIDs right now? Try our UUID Generator — supports v1, v4, v5, and v7 with batch generation and decoding, 100% in your browser.

Related Articles

View all articles