Back to Articles
#databases#architecture#postgres#distributed-systems#security#backend

Sequential IDs vs. UUIDs: Choosing the Right Identifier for Modern Systems

An architectural analysis of sequential integer IDs versus UUIDs, the tradeoffs of UUIDv4 and UUIDv7, and how to choose the right identifier for modern database systems.

14 min read
3,123 words

In relational database design, few conventions are as deeply ingrained as the auto-incrementing integer primary key (SERIAL, BIGSERIAL, or AUTO_INCREMENT).

At first glance, sequential integers offer appealing simplicity. Values like 1, 2, and 3 are intuitive to read, fit into compact 4- or 8-byte allocations, and serve as the default example across introductory database tutorials and framework starters.

However, in modern software architectures, relying on sequential integers as primary entity identifiers introduces significant structural drawbacks. They tightly couple domain identity to database-internal sequence generators, complicate client-server workflows, create security and privacy vulnerabilities by default, and introduce friction into distributed architectures.


1. Client-Side Generation: Reducing Latency and Enabling Universal Identity

With database-generated auto-incrementing IDs, the database is responsible for allocating the identifier. That means your application or frontend can’t reference, link, or pass around a new record until the database allocates the identifier and returns it.

Take an everyday example: creating an Order with several OrderItems.

The Sequential Round-Trip Overhead

With sequential identifiers, persisting parent and child records requires a serialized, multi-step round trip:

-- Step 1: Insert parent entity and wait for database-generated sequence
INSERT INTO orders (customer_id, status) 
VALUES ('c_982', 'pending') 
RETURNING id; -- Returns 1042

-- Step 2: Construct and execute child insertions using the returned identifier
INSERT INTO order_items (order_id, product_id, quantity) VALUES
  (1042, 'prod_1', 2),
  (1042, 'prod_2', 1);

This workflow forces the application to incur network latency waiting for the database to assign an identifier before related queries can even be constructed. When persisting hierarchical domain graphs—such as orders, line items, ledger entries, and audit logs—the cumulative latency overhead can become pronounced.

sequenceDiagram
    autonumber
    actor App as Application Tier
    participant DB as Database Engine

    App->>DB: INSERT Parent Order (awaiting ID)
    activate DB
    Note over DB: Generates sequential ID: 1042
    DB-->>App: Return generated ID: 1042
    deactivate DB
    App->>DB: INSERT Dependent Line Items (order_id: 1042)

The Client-Generated UUID Pattern

By utilizing UUIDs, identifier generation moves upstream to the application tier or client layer:

import { v7 as uuidv7 } from 'uuid';

const orderId = uuidv7();
const items = cart.map(item => ({
  id: uuidv7(),
  orderId: orderId, // Established immediately in memory
  productId: item.productId,
  quantity: item.quantity,
}));

// Persist the entire relational graph within a single atomic transaction:
await db.transaction(async (tx) => {
  await tx.insert(orders).values({ id: orderId, ... });
  await tx.insert(orderItems).values(items);
});

Because identifiers are known prior to persistence:

  • Decoupled Identity Generation: Complete object hierarchies can be constructed and cross-referenced entirely in memory before initiating database persistence, rather than waiting for an ID returned from a parent insert.
  • Optimistic UI Updates: Web and mobile applications can generate entity identifiers locally, update local application state instantly, and dispatch background persistence requests without blocking user interaction.
  • Offline-First Synchronization: Mobile and edge clients can create records while disconnected from the network, knowing that eventual synchronization is designed to make primary key collisions extraordinarily unlikely.
  • Universal Tracing and Telemetry: The entity identifier can be attached to distributed trace spans, message broker events (e.g., Kafka, RabbitMQ), and audit logs before database persistence occurs.

2. Security and Privacy: Reducing Enumeration and Information Disclosure

Sequential identifiers inherently reveal structural patterns and expose systems to predictable access behaviors.

Reducing Enumeration and Scraping Surface (Defense in Depth)

When primary identifiers increment sequentially, resources become readily discoverable through systematic enumeration:

GET /api/v1/invoices/10482
GET /api/v1/invoices/10483
GET /api/v1/invoices/10484

An Insecure Direct Object Reference (IDOR) is fundamentally a broken access control vulnerability. It is essential to be clear: UUIDs do not prevent IDOR, and unpredictable identifiers are never a substitute for authorization. As OWASP explicitly emphasizes, robust access control checks verified on every single request remain the primary, non-negotiable security control.

However, non-sequential identifiers serve as a vital secondary defense-in-depth against automated enumeration and mass scraping. If an authorization regression occurs or an endpoint lacks adequate rate limiting, sequential IDs make systematic harvesting trivial. A 128-bit UUID keyspace (2128 ≈ 3.4 × 1038 possible states) renders automated brute-force discovery of valid resource handles computationally infeasible.

An Important Privacy Caveat: UUIDv7 Exposes Creation Time

It is equally important to recognize that UUIDv7 is not completely opaque with respect to time.

Under RFC 9562, UUIDv7 embeds a 48-bit Unix millisecond timestamp in its most significant bits. Anyone inspecting a UUIDv7 can trivially extract the exact millisecond the identifier was created. If disclosing creation timing represents a business intelligence vulnerability or competitive concern, engineering teams should opt for UUIDv4 or encrypted application-layer tokens instead.

Preventing Business Intelligence Leakage via Serial Estimation

In statistical theory, estimating total volume or population size from a small sample of sequential identifiers is a well-established mathematical principle. When applied to software systems, sequential counters inadvertently disclose operational trends directly to competitors, market researchers, and outside observers.

Consider the operational intelligence exposed when primary keys increment sequentially:

  • Customer Acquisition Velocity: An observer who creates an account on Monday and receives id: 410, followed by a second account on Friday receiving id: 530, can deduce with reasonable accuracy that the platform is onboarding approximately 30 accounts per day.
  • Transaction and Revenue Volume: A merchant receiving invoice #1045 on March 1 and invoice #1200 on April 1 can estimate billing cycle volume.
  • Growth Trends and Seasonal Peaks: By periodically sampling identifiers across public-facing resources, external parties can reconstruct an organization’s transaction cadence over time.

While operational factors like transaction rollbacks, multi-node sequence caches, and batch allocations can introduce gaps into integer sequences (making exact counts an approximation), sequential keys still disclose directional telemetry and scale far more readily than high-entropy identifiers.


3. Distributed Consistency and Cross-Environment Safety

Sequential counters are confined to the scope of an individual table sequence within a single database instance. They lack context regarding the broader system topology.

While teams can mitigate integer collisions using namespaces, ID offsets, composite keys, or remapping tables, doing so introduces ongoing coordination overhead that UUIDs bypass naturally:

Operational Scenario Sequential Identifiers (BIGSERIAL) Universally Unique Identifiers (UUID)
Cross-Environment Data Sync Identifiers collide across dev, staging, and production environments without manual offset logic. High collision resistance. Seed data and sanitized production subsets migrate cleanly without key remapping.
Horizontal Sharding Demands distributed sequence coordinators (e.g., Ticket Servers, Snowflake IDs). Shards generate IDs autonomously without cross-node network dependencies.
Enterprise Data Consolidation Merging independent databases or acquired company schemas requires complex key remapping. Records preserve their original primary and foreign keys without collision risk.
Event Sourcing & ETL Pipelines Reconciling stream events across multiple regions presents sequence ordering anomalies. Entities maintain consistent, unambiguous identity across data warehouses and lakehouses.

When every record possesses an identifier designed to be unique across systems, database maintenance tasks, ETL ingestions (such as Apache Iceberg or Parquet lakehouse tables), and multi-region synchronizations operate without collision concerns.


4. Decoupling Record Identity from Storage Order and Lifecycle

In traditional sequence-driven schemas, an entity’s identifier is entangled with physical table insertion order and record count continuity.

This coupling often causes unnecessary operational friction when data is deleted:

  1. Sequence Gap Ambiguity: Stakeholders and compliance auditors may misinterpret gaps in sequential numbering (caused by deleted records or aborted transactions) as data loss or procedural irregularity.
  2. Fragile Query Logic: Application logic that relies on continuous ranges or assumptions like WHERE id > last_seen_id for pagination can fail when records are removed or transaction rollbacks occur.

UUIDs treat an identifier strictly as an immutable resource locator rather than a relative sequence position. Records can be pruned, archived, or purged freely according to data retention policies without concerns regarding index continuity or sequence gap semantics.


5. Architectural Selection: Aligning UUIDv7 and UUIDv4 with Storage Engines

Historically, database administrators rightfully raised performance concerns regarding UUIDs in relational databases.

Those concerns were centered on UUIDv4, which consists of 122 bits of pseudo-random data. When millions of random keys are inserted into a balanced tree (B-Tree or B+Tree), writes land unpredictably across arbitrary leaf pages. This causes:

  • Severe B-Tree index fragmentation
  • High rates of expensive disk page splits
  • Deteriorating cache hit ratios as working memory becomes thrash-heavy

Under RFC 9562, this challenge was addressed with UUIDv7.

Understanding how to deploy UUIDv7 and UUIDv4 effectively requires examining their internal bit structure and how database storage engines physically arrange data.

The RFC 9562 Bit Specification for UUIDv7

A standard UUID consists of exactly 128 bits. In UUIDv7, these bits are allocated into a time-ordered prefix and an entropy payload:

 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                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Breaking down the 128-bit layout:

  1. unix_ts_ms (48 bits): Big-endian unsigned integer representing the millisecond timestamp since the Unix epoch.
  2. ver (4 bits): UUID version field, binary 0111 (value 7).
  3. rand_a (12 bits): Pseudo-random bits (or optional sub-millisecond precision counter).
  4. var (2 bits): Variant field, binary 10 (per RFC 4122/9562).
  5. rand_b (62 bits): Additional pseudo-random bits.

After the timestamp, version, and variant fields, 74 bits remain (rand_a + rand_b). RFC 9562 allows implementations to populate these bits with pseudo-random data, sub-millisecond timestamps, monotonic counters, or a combination thereof.

UUIDv7 provides millisecond-resolution time ordering. As RFC 9562 (Section 6.2) notes, without an implementation-specific monotonic counter or sub-millisecond ordering scheme, UUIDs generated within the same millisecond are not guaranteed to reflect their creation order.


UUIDv7: The Standard for Relational Storage (PostgreSQL, MySQL, SQLite)

Relational database engines prioritize sequential write locality. Their primary index implementations rely on balanced trees (B-Trees / B+Trees).

Because the timestamp occupies the most significant bits:

  • Preserved Insertion Locality: UUIDv7 generally preserves much better insertion locality than UUIDv4, substantially reducing the random insertion pattern and severe page-splitting associated with v4.
  • Index Locality: Reduces the random insertion pattern and associated page-splitting pressure of UUIDv4 while retaining client-side generation.

Modern Database Support: PostgreSQL 18

As of PostgreSQL 18 (released in September 2025), uuidv7() is a native core built-in function:

-- Native in PostgreSQL 18+; use an extension or application-side generation on older versions:
CREATE TABLE accounts (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    organization_name TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

In MySQL 8+ and SQLite, UUIDs are typically stored as native 16-byte binary types (BINARY(16) or BLOB). Furthermore, because modern architectures emphasize generating identifiers client-side in the application tier (via libraries in TypeScript, Go, Rust, or Python), database engines simply store the standard 16-byte UUID value without needing in-database generation helpers.


Distributed Stores (Cassandra, ScyllaDB, DynamoDB): Access Patterns and Partitioning

Distributed wide-column stores (e.g., Apache Cassandra, ScyllaDB) and managed NoSQL databases (AWS DynamoDB) hash partition keys to distribute records across physical partitions and cluster nodes (Cassandra utilizing Murmur3Partitioner, DynamoDB using internal partition hashing).

Because of the avalanche effect inherent to hash functions, even a single-bit difference in a UUIDv7 timestamp produces a completely scrambled token across the hash ring. Therefore, using UUIDv7 as a partition key distributes writes across physical partitions just as uniformly as UUIDv4.

However, in distributed databases, key selection is fundamentally driven by access patterns and query cardinality:

  • UUIDv7 as a Clustering / Sort Key: In compound primary keys—such as Cassandra’s (partition_key, clustering_key) or DynamoDB’s Partition Key (PK) + Sort Key (SK)—UUIDv7 is an excellent choice when chronological ordering is useful. Within a partition (e.g., PK = tenant_id, SK = uuidv7()), the UUIDv7 sort or clustering key provides naturally time-ordered values, enabling efficient range queries over recent or historical records (WHERE tenant_id = ? AND event_id > ?) without the security liabilities of legacy TimeUUID (UUIDv1).
  • UUIDv4 for Opaque Partition Keys: When an entity’s creation timing should remain private and chronological clustering within a partition is unnecessary, UUIDv4’s 122 bits of pure pseudo-random data provide complete temporal opacity.

The Genuine Engineering Trade-Off: Storage and Cache Footprint

An objective architectural evaluation must acknowledge the primary cost of transitioning from integers to UUIDs: storage and memory footprint.

  • A standard UUID occupies 16 bytes, compared to 4 bytes (INT) or 8 bytes (BIGINT).
  • In high-volume relational schemas with hundreds of millions of rows, tables with multiple foreign key references, and numerous secondary indexes, storing 16-byte identifiers increases disk footprint and expands the active working set within the database cache pool (e.g., PostgreSQL shared_buffers or MySQL InnoDB buffer pool).

This is a real trade-off. However, for many distributed and client-heavy architectures, trading memory capacity for client-side generation, decoupled identity, and defense-in-depth against data enumeration represents a favorable investment. For memory-constrained, write-heavy workloads with dozens of secondary indexes per table, engineering teams should factor index sizing into capacity planning.


6. Unambiguous Observability: Instant Search Across Logs, grep, and Elasticsearch

One of the most practical operational advantages of universally unique identifiers is how significantly they streamline debugging, log aggregation, and system observability across microservices and data pipelines.

The Problem with Searching Short, Ambiguous Identifiers

When troubleshooting an incident in production involving an auto-incrementing integer identifier—such as order #42 or account #1042—attempting to locate related entries in raw server logs or centralized logging platforms quickly becomes an exercise in wading through noise:

# Attempting to track an issue with Order #42 in server logs:
grep -rn "42" /var/log/app/

This search produces an overwhelming volume of false positives. It matches HTTP status codes (422 Unprocessable Entity), memory statistics, thread counts, port numbers (4242), timestamps (14:42:00), line numbers, and unrelated entities across different database tables that share the same numerical key.

Even within structured search platforms like Elasticsearch, OpenSearch, Splunk, or Datadog, querying small integers across unstructured message strings, exception stack traces, or raw JSON payloads requires restrictive field filtering. If an unformatted log statement records a message such as "Failed to process shipment for 42", isolating that specific occurrence amidst thousands of log events is remarkably challenging.

High-Cardinality Search as an Observability Primitive

In contrast, a 128-bit UUID provides a vast, non-repeating namespace with high pseudo-random bit distribution:

# Isolating an exact transaction across multi-gigabyte log archives:
grep -rn "0191e4f2-938b-7000-8fa2-34821a8d11c0" /var/log/

Because that identifier is designed to be probabilistically unique, every matching line in raw text logs is virtually guaranteed to belong to that exact entity or transaction without false-positive clutter.

In Elasticsearch, OpenSearch, and distributed log aggregators:

  • Universal Free-Text Search: Developers and support engineers can paste or search for the exact UUID across indices, nested JSON payloads, and service boundaries without ambiguity. (Note: Standard full-text analyzers break terms on hyphens; ensure identifier fields are mapped as keyword types in index mappings or queried using quoted phrase syntax "0191e4f2-938b-..." to prevent tokenization splitting).
  • Cross-Service Correlation: From client-side exception trackers (e.g., Sentry) through API gateways, Kafka/RabbitMQ message brokers, worker processes, and database logs, the UUID serves as an immutable trace key connecting the full transaction lifecycle.
  • Zero Ambiguity in Stack Traces: When exceptions occur and raw SQL statements or parameter arrays are dumped into error outputs, searching for a UUID immediately surfaces the relevant failure without false-positive clutter.

7. What About Snowflake IDs, ULID, and KSUID?

When evaluating identifier strategies, alternatives such as Twitter Snowflake, Sonyflake, ULID, and KSUID frequently arise.

These specifications provide well-engineered solutions for targeted requirements:

  • Snowflake IDs: Compact a 41-bit timestamp, 10-bit machine/datacenter identifier, and 12-bit sequence counter into a 64-bit integer (BIGINT).
  • ULID (Universally Unique Lexicographically Sortable Identifier): Encodes a 128-bit sortable identifier into a 26-character Crockford Base32 string.
  • KSUID (K-Sortable Unique ID): Delivers 160 bits of timestamped payload with 27-character base62 formatting.

While these alternatives have legitimate niches, for the vast majority of production applications, UUIDv4 and UUIDv7 avoid unnecessary operational complexity:

  1. No Worker ID Coordination: Snowflake schemes require assigning and maintaining unique machine identifiers across nodes (via etcd, Consul, or environment variables). If two container instances accidentally share a worker ID during autoscaling, key collisions can occur. UUIDs require zero worker-node coordination.
  2. No JavaScript 64-Bit Integer Traps: Snowflake IDs are 64-bit integers. JavaScript’s native Number type can only safely represent integers up to 253 - 1 (Number.MAX_SAFE_INTEGER). Emitting 64-bit numbers in JSON APIs frequently leads to silent client-side data truncation unless the engineering team strictly enforces string casting across all API boundaries.
  3. Official International Standardization: UUID is an established standard (RFC 4122 and RFC 9562) natively recognized across relational engines, language runtimes, and serialization libraries.

Architectural Comparison Matrix

Evaluation Criteria Sequential Integers (SERIAL / BIGINT) Random UUID (UUIDv4) Time-Ordered UUID (UUIDv7)
Client-Side Generation Typically requires database allocation or an application-side coordination/generation scheme Supported Supported
B-Tree Index Locality Highly efficient (append-only) Degraded (random page splits) Substantially improved (append-friendly)
Distributed Hash Partitioning Vulnerable to write hotspots Excellent (hash dispersion) Excellent (hash dispersion)
Clustering / Sort Key within Partition Requires sequence coordination Random / non-chronological Excellent (chronologically ordered)
Enumeration / Scraping Resistance Low (predictable values) High (unpredictable namespace) High (unpredictable; exposes creation time)
Log & Text Search (grep, Elasticsearch) High noise / false positives Unambiguous / high-cardinality Unambiguous / high-cardinality
Storage & Memory Footprint Minimal (4–8 bytes per row/index) Moderate (16 bytes per row/index) Moderate (16 bytes per row/index)
Cross-System Collision Resistance Limited to single table sequence Probabilistic collision resistance Probabilistic collision resistance
Chronological Sortability Relative to insertion sequence Non-sortable Naturally sortable by millisecond

Conclusion

The choice of entity identifier is an architectural decision with meaningful trade-offs:

  • Where Sequential BIGINT Remains the Right Choice: For internal metrics tables, append-only logs, high-throughput analytics, or single-node relational databases where entities are never exposed to public APIs and buffer-pool cache density is paramount, sequential BIGINT remains an exceptionally compact, simple, and battle-tested choice.
  • Where UUIDv7 Excels: For user-facing web applications, relational schemas requiring client-side ID generation, multi-tenant databases, and distributed clustering/sort keys, UUIDv7 provides time-ordered index locality without sequence coordination.
  • Where UUIDv4 Excels: When identifiers must remain completely opaque and creation timestamps must not be disclosed.

For modern applications building across clients, microservices, and distributed environments, standardizing on UUIDs makes cross-system key collisions extraordinarily unlikely, reinforces defensive security boundaries, and enables seamless client-driven orchestration from day one.