← Back to Blog
Developer8 min read

UUID Guide for Developers: v4, v7, ULIDs, and When to Use Each

UUID v4 has been the default for years, but UUID v7 and ULIDs have changed the calculus. This guide covers every practical UUID variant, the B-tree index fragmentation problem, and a clear decision framework for choosing identifiers.

Tanvrit Team
1 March 2026 · Engineering
Share

At some point every developer reaches for a UUID when they need a unique identifier. For most of the past decade, UUID v4 has been the default choice. But the landscape has changed: UUID v7 was formally specified in 2024, ULIDs have grown in popularity, and the performance implications of identifier choice for database primary keys are better understood than ever. This guide covers every practical variant so you can make an informed decision rather than defaulting to v4 by habit.

What Is a UUID?

UUID stands for Universally Unique Identifier. It is a 128-bit label defined by RFC 4122 (now updated by RFC 9562 in 2024) that is designed to be unique across space and time without requiring a central coordination authority. The canonical text representation uses lowercase hexadecimal with hyphens separating five groups: xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx, where M encodes the version and N encodes the variant.

A UUID in text form is always 36 characters (32 hex digits + 4 hyphens), or 32 characters when stored without hyphens. In binary it occupies exactly 16 bytes. The specification defines multiple versions with different strategies for generating the 128 bits — each with different tradeoffs between uniqueness guarantees, sortability, and privacy.

UUID v1: Time + MAC Address

UUID v1 generates identifiers from a combination of the current timestamp (in 100-nanosecond intervals since October 15, 1582 — the Gregorian calendar reform date), a clock sequence counter, and the MAC address of the generating network interface.

The MAC address inclusion is the problem. It directly embeds a hardware identifier that uniquely identifies the machine that generated the UUID. This is a significant privacy leak: anyone with a UUID v1 can determine the MAC address of the server that produced it, and therefore correlate UUIDs generated on the same machine. In 2005, the Sasser worm was partially tracked because its authors used UUID v1 values that revealed their development machines' MAC addresses.

Unless you specifically need time-extractable UUIDs generated on known hardware, avoid UUID v1 in new systems. UUID v7 provides time ordering without the MAC address exposure.

UUID v4: Random — The Ubiquitous Default

UUID v4 generates all 128 bits randomly, with 6 bits reserved for the version (4) and variant (RFC 4122) markers. The result is effectively 122 bits of randomness. When generated from a cryptographically secure random number generator, the probability of a collision between two randomly generated v4 UUIDs is approximately 1 in 5.3 × 1036.

To put the collision probability in practical terms: if you generated 1 billion UUIDs per second continuously, you would expect to see your first collision after approximately 85 years. For nearly all applications, UUID v4 collision probability is not a real concern. The risk becomes theoretical only at astronomical scales.

UUID v4 is the right choice when you need a unique identifier, you do not need the IDs to be sortable, and simplicity matters. Its major weakness for databases is that fully random IDs fragment B-tree indexes — more on this below.

UUID v5: Deterministic from a Namespace and Name

UUID v5 generates a deterministic UUID by hashing a namespace UUID and a name string using SHA-1. Given the same namespace and name, you always get the same UUID. UUID v3 is identical except it uses MD5 instead of SHA-1 — avoid v3 in new code since MD5 is cryptographically broken. Use v5 instead.

Practical use cases for UUID v5: generating a stable UUID for a user from their email address, creating deterministic IDs for test fixtures, producing a consistent identifier for a URL across systems, or creating database foreign keys for entities that are identified by a natural key rather than a surrogate key. The predefined namespaces in the spec include DNS, URL, OID, and X.500.

import { v5 as uuidv5 } from 'uuid';

const DNS_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';

// Same input always produces the same UUID
const id = uuidv5('tanvrit.com', DNS_NAMESPACE);
// => "some-deterministic-uuid-here"

UUID v7: Time-Ordered Random — The Modern Default

UUID v7 was formally specified in RFC 9562 (May 2024). It combines a 48-bit Unix millisecond timestamp in the most-significant bits with 74 bits of random data in the remaining bits (after version and variant markers). The result is a UUID that:

  • Sorts chronologically when compared as strings or bytes — UUIDs generated later sort after UUIDs generated earlier
  • Has no privacy-leaking hardware identifiers (unlike v1)
  • Retains strong uniqueness guarantees from the random component
  • Is binary-compatible with the UUID format — 128 bits, same wire format

The timestamp prefix means that UUIDs v7 generated in sequence will be stored in roughly sequential order in a B-tree index. This is the critical database performance advantage over v4.

ULID: Lexicographically Sortable Identifiers

ULID (Universally Unique Lexicographically Sortable Identifier) is an alternative identifier format, not part of the UUID spec. A ULID encodes a 48-bit millisecond timestamp followed by 80 bits of random data into a 26-character Crockford Base32 string: 01ARZ3NDEKTSV4RRFFQ69G5FAV.

ULID advantages over UUID: the Crockford Base32 encoding is more compact (26 chars vs 36 chars), case-insensitive, and excludes ambiguous characters (I, L, O, U) that can be confused visually. ULIDs sort lexicographically by default in any system that does string comparisons. The timestamp is human-readable when you need to eyeball when something was created.

ULID disadvantages: it's not an IETF standard, library support is less universal than UUID, and some databases and ORMs have first-class UUID support but not ULID support. UUID v7 now covers ULID's primary advantage (time-sortability) with the backing of an official standard.

Comparison: v4 vs v7 vs ULID vs Nanoid vs Auto-Increment

Here is a practical breakdown of the main identifier options:

  • Auto-increment integer: simplest, smallest (4 or 8 bytes), perfectly sequential for indexes. Exposes record counts to clients (IDOR risk), cannot be generated client-side without a database round-trip, fails for distributed systems without a sequence coordinator.
  • UUID v4: 16 bytes binary / 36 chars text, no sortability, universal library support, random fragmentation of B-tree indexes at high insert rates. Best for low-write-volume tables or when sortability is irrelevant.
  • UUID v7: 16 bytes binary / 36 chars text, time-sortable, RFC standard, growing library support. The best default for new systems that need UUIDs and care about database insert performance.
  • ULID: 16 bytes binary / 26 chars text, time-sortable, human-friendlier string format. Good choice if you prefer the text representation or need Crockford Base32 encoding specifically.
  • Nanoid: customizable length (default 21 chars), URL-safe alphabet, not time-sortable. Excellent for short user-facing IDs (URL slugs, share links) where compactness matters more than database performance.

UUIDs as Database Primary Keys

The choice between UUID and auto-increment as a primary key has real performance implications, and it depends heavily on which UUID version you choose.

A database B-tree index (used by PostgreSQL, MySQL, SQLite, and most relational databases for primary key indexes) maintains sorted order for fast lookups. When rows are inserted with sequential keys (like auto-increment), each new row appends to the end of the index — the optimal case. When rows are inserted with fully random UUID v4 keys, each insert lands at a random position in the index, forcing page splits and random I/O. At high insert rates with large tables, this can reduce write throughput by 20–50% compared to sequential keys.

UUID v7 largely solves this. Because the timestamp prefix ensures that newly generated UUIDs sort after previously generated ones, inserts are nearly sequential at the millisecond granularity. The 80 bits of random data within the same millisecond provide uniqueness without disrupting the sequential insert pattern. Benchmarks from the PostgreSQL community show UUID v7 approaching auto-increment performance for write-heavy workloads.

Storage format matters too. UUIDs stored as VARCHAR(36) (the text form with hyphens) occupy twice as much space as BINARY(16) and compare more slowly. If your database supports a native UUID type — PostgreSQL does with its uuid column type, which stores 16 bytes — always prefer it. MySQL lacks a native UUID type, so use BINARY(16) and store UUIDs without hyphens, not VARCHAR(36).

Generating UUIDs in Code

JavaScript / TypeScript

// Built-in: crypto.randomUUID() — v4 only, available in Node 14.17+ and all modern browsers
const id = crypto.randomUUID();
// "110e8400-e29b-41d4-a716-446655440000"

// npm package: uuid — supports v1, v3, v4, v5, v7
import { v4 as uuidv4, v7 as uuidv7 } from 'uuid';
const v4Id = uuidv4();
const v7Id = uuidv7();

// ULID
import { ulid } from 'ulid';
const id = ulid();  // "01ARZ3NDEKTSV4RRFFQ69G5FAV"

// nanoid
import { nanoid } from 'nanoid';
const shortId = nanoid();  // "V1StGXR8_Z5jdHi6B-myT"

Python

import uuid

# v4 — random
id_v4 = uuid.uuid4()
print(str(id_v4))  # "110e8400-e29b-41d4-a716-446655440000"

# v5 — deterministic
DNS = uuid.NAMESPACE_DNS
id_v5 = uuid.uuid5(DNS, "tanvrit.com")

# UUID v7 — use the uuid7 package
# pip install uuid7
from uuid7 import uuid7
id_v7 = uuid7()

PostgreSQL

-- Built-in UUID v4 (requires pgcrypto extension in older versions)
SELECT gen_random_uuid();

-- UUID v7 (PostgreSQL 17+ has pgcrypto support; otherwise use pg_uuidv7 extension)
SELECT uuid_generate_v7();  -- requires pg_uuidv7 extension

-- Using UUID as primary key
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL UNIQUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

When to Use Which

A practical decision guide:

  • New API or service with a database: UUID v7 as the default. You get standard-compliant UUIDs, opaque IDs that don't expose sequential record counts, distributed generation without coordination, and good index performance.
  • Existing system already using UUID v4: No need to migrate. UUID v4 is fine unless you are experiencing measurable insert performance degradation on large, write-heavy tables.
  • Short user-visible IDs (URLs, share codes): nanoid with a custom alphabet and appropriate length. Shorter and more readable than UUIDs.
  • Deterministic ID from a natural key: UUID v5 with an appropriate namespace.
  • Simple internal service, not distributed: Auto- increment is completely reasonable. The benefits of UUIDs apply primarily to distributed systems and public-facing APIs.

Common Mistakes

The most frequent UUID mistakes in production code:

  • Math.random() for IDs: Math.random() is not cryptographically random and should never be used to generate identifiers. Use crypto.randomUUID() or a proper UUID library.
  • VARCHAR(36) instead of a UUID type or BINARY(16): Text storage is 2× the size and slower to index. Always use the most compact storage format your database supports.
  • Mixing UUID versions in the same column: If you later need to sort by creation time using the UUID, a column containing a mix of v4 and v7 values will not sort correctly. Pick one version per column.
  • Using UUID v1 in new code: The MAC address exposure is a privacy problem with no upside over UUID v7.

Generate UUID v4, v7, and other formats instantly in your browser with Tanvrit's UUID generator — no installation required. Open the UUID Generator →

UUIDUUID v4UUID v7ULIDnanoiddatabase primary keysunique identifiers
← Back to Blog
Share