DatabaseArchitectureSecurity

The Complete Guide To Identifiers - Performance, Privacy, And Compliance In Modern Systems

ID selection is a critical early design step; poor choices cause long-term failures in DB sorting under heavy writes, system clock drift reactions, & public metadata leaks via API payloads/URLs.

The Complete Guide To Identifiers - Performance, Privacy, And Compliance In Modern Systems — cover illustration

Introduction

Choosing how to identify records is one of those quiet, early design steps that can silently break an application years down the line. The layout you pick dictates database sorting behavior under heavy write loads, how your services react when system clocks inevitably drift, and exactly what kind of internal metadata you leak to the public whenever an ID ends up in an API payload or URL.

Core principles

Before comparing formats, let's establish a few basic realities of systems architecture:

  1. Identifiers are not permission slips. RFC 9562 explicitly warns against assuming UUIDs are too hard to guess. Never rely on them as credentials or proof of authorization.
  2. If an ID is sorted by time, it leaks time. Making identifiers sequential exposes when they were created. If your application requires birth times to remain private, RFC 9562 recommends stick to fully random UUIDv4.
  3. Index performance is tied to representation. RFC 9562 defines UUIDs as 16-byte (128-bit) values encoded in network byte order, but how your database actually indexes and processes those bytes varies by engine.

UUIDs: versions, specifications, and sentinel values

UUID version overview (RFC 9562)

Under RFC 9562, which updated the classic RFC 4122 standard, the version field indicates the creation algorithm. Some are standard; others are historical footnotes:

  • Version 0: Unused.
  • Version 1: Time-based, combining a 60-bit Gregorian timestamp with a physical hardware MAC address (or pseudo-random node).
  • Version 2: Reserved for "DCE Security" (POSIX UIDs/GIDs), with the layout details managed outside RFC 9562.
  • Versions 3 & 5: Name-based (deterministic) using MD5 (v3) or SHA-1 (v5).
  • Version 4: Fully random or pseudorandom.
  • Version 6: A reordered version of Version 1 designed to make Gregorian timestamps sortable.
  • Version 7: The modern standard, combining a 48-bit Unix millisecond epoch timestamp with random bits.
  • Version 8: A sandbox for entirely custom layouts.
  • Versions 9 through 15: Reserved for future standardization.

UUIDv2 (why it's omitted and what it represents)

You almost never see UUIDv2 in modern web apps, and for good reason: RFC 9562 leaves its implementation details out of scope.

If you dig into the Open Group's old DCE 1.1 security specification, you find that v2 replaces the lowest bits of the timestamp with local POSIX identifiers (like a user or group ID) to restrict uniqueness within a specific DCE "cell" or domain context. Because it embeds raw local user and group IDs, it leaks OS-level metadata by design. Unless you are maintaining deep legacy integrations with ancient distributed computing systems, you have no reason to use it.

Nil and Max UUIDs as sentinel values

The specification reserves two extreme values to act as sentinels:

  • Nil UUID (00000000-0000-0000-0000-000000000000): Represents the absence of a value.
  • Max UUID (FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF): Represents the maximum possible UUID, often used as a sorting end-cap or list sentinel.

Treat these strictly as internal sentinels. It is a good practice to validate incoming payloads at your API gateway and reject these values if a client attempts to pass them in as regular entity IDs. If you need to represent "no value" in a database, use a standard nullable column rather than smuggling a Nil UUID into your primary key, unless you have a highly constrained performance requirement that justifies it.

The identifier ecosystem

Comparison of production formats

ID type Size Time component Sorting intent External metadata leakage Reference
UUIDv1 128b 60-bit 100ns time + clock sequence + node Time-based High (time + node) RFC 9562
UUIDv2 (DCE) 128b DCE-defined DCE semantics High (local identifiers) RFC 9562 + Open Group DCE
UUIDv3 128b None None Depends on name secrecy RFC 9562
UUIDv4 128b None None Low (random) RFC 9562
UUIDv5 128b None None Depends on name secrecy RFC 9562
UUIDv6 128b 60-bit 100ns time (reordered v1) Time-ordered High (time-based) RFC 9562
UUIDv7 128b 48-bit Unix ms timestamp Time-ordered Medium (time) RFC 9562
ULID 128b 48-bit ms + 80-bit randomness Lexicographically sortable Medium (time) ULID spec
KSUID 160b 32-bit seconds + 128-bit payload K-sortable Medium (time) segmentio/ksuid
ObjectId 96b 4-byte seconds + 5-byte process-unique + 3-byte counter Roughly time-ordered Medium (time) MongoDB ObjectId spec
XID 96b 4-byte seconds + machine + pid + counter Time-prefixed High (machine/pid) rs/xid
NanoID (default) 21 chars None None Low (random) ai/nanoid
TypeID String UUIDv7-based suffix (base32) K-sortable Medium (time) jetify TypeID
TSID (tsid-creator) 64b 42-bit ms since 2020-01-01 + 22-bit random Time-ordered High (time + structure) tsid-creator
Snowflake (Twitter) 64b 41-bit ms + machine/worker + sequence Time-ordered High (time + topology) Twitter layout descriptions
Sonyflake 64b Time + sequence + machine-id Time-ordered High (time + topology) sony/sonyflake
Firebase Push Keys String Timestamp-based Chronological list keys Medium (time correlation) Firebase docs
CUID2 24 chars Time included among entropy sources Not time-ordered Low (designed to hide data) paralleldrive/cuid2
SNID (experimental) 128b Claims UUIDv7-compatible ordering Protocol-defined Varies SNID docs.rs

NanoID entropy math

With a 64-character default alphabet and a length of 21 characters, the math works out as follows:

$$21 \times \log_2(64) = 126\text{ bits of entropy}$$

This matches the collision safety profile of UUIDv4, without the rigid hex-and-hyphen formatting.

Locality-first alternatives (the "ObjectId" family)

When write throughput is your primary constraint, traditional random UUIDs can be problematic for database B-trees because they force random writes across the entire index. To solve this, a family of time-first formats has emerged. They prioritize database locality by starting with a timestamp, then appending random bytes to ensure uniqueness.

Common formats and how they differ

  • MongoDB ObjectId: A compact 12-byte (96-bit) format split between a 4-byte timestamp, a 5-byte process-unique random value, and a 3-byte counter. The spec notes that the random portion doesn't need to be cryptographically secure.
  • XID: A 12-byte variant that implements the ObjectId concept but uses a different, highly efficient base32 string serialization to avoid binary parsing overhead.
  • KSUID: A larger 20-byte (160-bit) identifier. It uses a 32-bit second-precision timestamp followed by a 128-bit random payload, providing high randomness at the cost of a larger storage footprint.
  • ULID: A 128-bit format that fits cleanly into existing UUID columns. It uses a millisecond-precision 48-bit timestamp and 80 bits of randomness, serialized as a 26-character base32 string.
  • UUIDv7: The standardized modern solution. It has the same 128-bit size as older UUIDs but puts a 48-bit millisecond timestamp upfront, resolving database clustering problems natively.
  • Firebase Realtime Database Push Keys: These are string-based, chronologically sorted keys generated by the client. While Firebase docs don't specify the exact bit layout, they guarantee alphabetical ordering based on timestamps.
  • Snowflake (Twitter's layout): A 64-bit format designed for distributed generation. A common layout uses a 41-bit millisecond timestamp, 10 bits of machine/worker ID, and 12 bits of sequential counter.
  • Sonyflake: A 64-bit variation of Snowflake that shifts bit allocations (using more bits for device IDs and a larger timestamp window) to support highly distributed setups without a central coordinator.

Database performance metrics

Native UUIDv7 support in PostgreSQL 18

With the release of PostgreSQL 18, UUIDv7 is now a first-class citizen. The engine supports native generation using uuidv7() (along with uuidv4() as an alias for the old gen_random_uuid()).

It also introduces uuid_extract_timestamp(uuid), which allows you to extract the embedded creation time directly from any v1 or v7 UUID. Just keep in mind that the extracted timestamp depends on the accuracy of the client generator that built the UUID; do not treat it as an immutable audit log.

Real-world benchmark analysis

Analyzing public benchmarks testing heavy insert loads under PostgreSQL 18 reveals a clear performance gap between random and sequential IDs. In a single-threaded insert run:

  • Throughput: Sequential UUIDv7 sees roughly a 16% to 17% increase in operations per second compared to random UUIDv4.
  • Latency: Average write latency drops by about 33% under the same test conditions.

The reason for this isn't that generating the bytes is faster; it's the B-tree index structure. Because UUIDv7 is sequential, Postgres appends new rows to the end of the index page. With UUIDv4, writes are scattered randomly, forcing the database to constantly split B-tree pages and read/write random blocks from disk.

Security and privacy trade-offs

The RFC 9562 security baseline

  • Identifiers are not security tokens. If an attacker can guess or predict an ID, and that ID is the only thing protecting a resource, your app is broken. RFC 9562 explicitly warns against using UUIDs as credentials.
  • MAC addresses are a liability. Early UUID specs (v1) baked in the host machine's physical MAC address. Exposing these gives attackers an easy way to map your internal network topology. Modern implementations should use cryptographically secure pseudorandom numbers (CSPRNG) instead.
  • UUIDv4 is the baseline for secrecy. If you need to generate identifiers for password reset links or other security-sensitive operations, use UUIDv4 to ensure there is no predictable structure.

The illusion of secrecy with deterministic IDs (UUIDv5)

Don't mistake a SHA-1 namespace-based UUIDv5 for a secure hash. If an attacker can guess the namespace and the input value, they can generate the exact same ID. It is useful for deduplication, but useless for secrecy.

Compliance considerations

Payment card security (PCI SSC guidelines)

The PCI SSC guidelines specify that payment tokens must not be mathematically derived from the Primary Account Number (PAN). While there's no rule banning timestamps in identifiers, keeping external tokens entirely random (like UUIDv4) minimizes correlation risks and ensures you can't accidentally reconstruct a card number.

Data privacy and tracking (GDPR)

GDPR defines personal data very broadly. While a random string of numbers isn't personal data in a vacuum, European regulators (like the UK ICO) point out that online identifiers can easily become personal data when combined with other system logs. Exposing time-ordered IDs allows an observer to link events and trace user activity over time, potentially pulling those IDs into GDPR scope.

Security standards (ISO 27001/27002)

These standards are highly authoritative but paywalled, which leads to a lot of online myths. ISO does not mandate or forbid specific identifier formats. They care about data classification, access control, and minimizing exposure. Decisions like using opaque IDs externally are implementation details that support those broader security controls.

Decision framework

External-facing identifiers (APIs and URLs)

We recommend using opaque, non-sequential IDs like UUIDv4, NanoID, or CUID2 here. It prevents third parties from reverse-engineering business metrics, such as sign-up volumes or daily transactions, and eliminates the risk of chronological tracking.

Internal database primary keys

For tables with high write volume, time-ordered formats like UUIDv7, ULID, or Snowflake-style IDs are highly effective. They keep index page splits minimal and write throughput high, all while allowing you to generate unique keys in a distributed environment without needing a central lock.

The hybrid approach: dual-ID architecture

The most robust architectural pattern is often to use both:

  • Store a sequential internal_id (like UUIDv7) for database joins, indexing, and clustering.
  • Generate an opaque public_id (like UUIDv4) to expose through public APIs and URLs.

This cleanly separates internal performance optimization from external security and privacy requirements. It does require slightly more storage, but it resolves the trade-offs on both sides.

Operational considerations

Managing clock drift and monotonicity

Because time-ordered IDs rely on system clocks, clock rollbacks or NTP sync jumps pose a real risk to ordering and uniqueness. RFC 9562 provides strategies for handling this:

  • Wait for the clock to catch up before generating the next ID, or
  • Increment a monotonic counter in the random bits block if the timestamp matches or falls behind the previous value.

Make sure the library you choose implements proper monotonic guards.

Efficient database storage

Always store UUIDs in their native binary formats (like the 16-byte UUID column in Postgres or BINARY(16) in MySQL) rather than as 36-character strings. It saves significant disk space and speeds up index lookups dramatically.

Experimental formats

SNID

You will occasionally run into formats like SNID that claim to offer UUIDv7 compatibility while packing in custom feature sets like logical "families." Treat these with caution. Unless they are backed by an open standard or widely adopted in your runtime's ecosystem, you risk locking yourself into a custom library that might be abandoned next year.

Conclusion

There is no single "best" identifier. The trick is recognizing where the boundaries of your system lie. Keep your database indexes healthy by using sequential keys internally, protect your users and your business metrics by using opaque random keys externally, and always treat UUIDs as names, never as permission slips.

References

Standards / specifications (primary)

Databases (vendor documentation)

Benchmarks (public, reproducible repos)

Identifier formats (project/vendor specifications)

Compliance / legal / governance (authoritative + source-access notes)

Ahmet Fatihoglu

Ahmet Fatihoglu

Senior Software Developer & Architect

10+ years of experience designing distributed systems, enterprise applications, and full-stack solutions with .NET, TypeScript, and modern architectures. Microsoft Azure Data Engineer Associate.

Continue reading