Back to Practical SQL Queries
Q23Question 23 of 34

What are the UUIDv7 caveats

Key Concepts
  • UUIDv7 embeds a Unix timestamp in the top 48 bits for B-tree locality, but clock skew across nodes breaks strict ordering.
  • Within one node ids are reliably increasing; the approximate-ordering caveat applies only across multiple generators.
  • Store as native uuid (PostgreSQL) or BINARY(16) (MySQL), not CHAR(36), which more than doubles storage and index size.
  • The leading 48 bits are a readable creation timestamp, so UUIDv7 leaks when a record was created.
  • UUIDv7 is an identifier, never a secret: use 256-bit SecureRandom for session tokens and password-reset tokens.
  • For strict global ordering, use an explicit authoritative timestamp column or a database sequence.
  • Use UUIDv7 as an internal key and issue a separate cryptographically random token for security-sensitive surfaces.

Definition

UUIDv7 is approximately ordered (clock skew breaks global sequence), must be stored as 16-byte binary not CHAR(36), and leaks creation time so it is an identifier, never a secret.
  • Approximate ordering – the time prefix comes from each node's wall clock; clock skew across nodes means UUIDv7 is not a reliable distributed sequence or global causal order.
  • Compact storage – store as uuid (PostgreSQL) or BINARY(16) (MySQL), not as CHAR(36), which roughly triples storage and bloats every index.
  • Time leakage – the leading 48 bits are a readable creation timestamp, so a UUIDv7 reveals when the record was created and must never be used as a secret, session token, or authorization credential.
  • Within one node – ids are reliably increasing; the ordering concern is only across multiple generators.
  • Separate token – if you need both an ordered key and an opaque public token, use UUIDv7 internally and issue a cryptographically random token for security.

A UUIDv7 reference number at National Bank of India has three warning labels. First, each branch stamps the number using its own wall clock, so if two branches' clocks disagree, you cannot trust the numbers to tell you which transaction truly happened first. Second, write the number in its compact binary form, not as a long 36-character string, or you waste filing space. Third, the number openly contains the timestamp, so anyone reading it learns when the record was created: it can identify a record but must never act as a password.

Understanding the Concepts

UUIDv7 embeds a Unix timestamp in the most significant 48 bits, followed by random bits. This makes the ids roughly time-ordered, which gives B-tree index locality and avoids the random-insert fragmentation of UUIDv4.

JAVA
package com.bank.model;

import java.util.UUID;

public class UUIDv7Demo {
    public static void main(String[] args) {
        // Java 17+ does not have native UUIDv7; use a library
        // Example using com.fasterxml.uuid:java-uuid-generator
        // UUID id = Generators.timeBasedEpochGenerator().generate();

        // The first 48 bits are the Unix timestamp in milliseconds
        // Remaining bits are random
        // Result: 019234ab-cdef-7xxx-xxxx-xxxxxxxxxxxx
    }
}

Why is UUIDv7 not a distributed sequence?

The time prefix comes from the generating node's wall clock. Within one node, ids are reliably increasing. But across nodes, clocks drift.

DIAGRAM
Node A clock: 10:00:00.000 ──▶ generates UUID_A
Node B clock: 10:00:00.003 ──▶ generates UUID_B (3ms later by B's clock)

But what if Node A's clock is 5ms ahead of true time?
Node A actual:  09:59:59.995 ──▶ UUID_A timestamp says 10:00:00.000
Node B actual:  10:00:00.003 ──▶ UUID_B timestamp says 10:00:00.003

UUID_A < UUID_B by timestamp, but UUID_B was actually created AFTER UUID_A
Clock skew: 5ms difference makes the ordering unreliable across nodes

For true ordering, use an explicit authoritative timestamp column, a real sequence, or logical clocks.

Why must UUIDv7 be stored compactly?

A UUID is 128 bits (16 bytes). Stored as CHAR(36), it becomes 36 bytes plus overhead: more than double the size.

SQL
-- Wrong: CHAR(36) wastes space and bloats indexes
CREATE TABLE transactions (
    id CHAR(36) PRIMARY KEY,  -- 36+ bytes per row and per index entry
    amount NUMERIC(18,2)
);

-- Correct in PostgreSQL: native uuid type (16 bytes)
CREATE TABLE transactions (
    id UUID PRIMARY KEY,  -- 16 bytes
    amount NUMERIC(18,2)
);

-- Correct in MySQL: BINARY(16)
CREATE TABLE transactions (
    id BINARY(16) PRIMARY KEY,  -- 16 bytes
    amount DECIMAL(18,2)
);

Since the primary key is embedded in every secondary index in InnoDB, a CHAR(36) key bloats the entire table's index footprint and wastes buffer-pool memory.

How much storage difference does it make?

Rajesh measured at National Bank of India with 10 million transactions.

DIAGRAM
┌──────────────┬──────────────┬───────────────┐
│ Storage Type │ PK Size      │ Index Overhead│
├──────────────┼──────────────┼───────────────┤
│ BINARY(16)   │ 160 MB       │ +16 bytes/row │
│ CHAR(36)     │ 360 MB       │ +36 bytes/row │
│ Difference   │ +125%        │ +125% per idx │
└──────────────┴──────────────┴───────────────┘

Convert to text only at the application edge for display.

Why does UUIDv7 leak creation time?

The first 48 bits are a plainly decodable Unix timestamp in milliseconds.

JAVA
package com.bank.util;

import java.time.Instant;
import java.util.UUID;

public class UUIDv7TimeExtractor {

    public static Instant extractTimestamp(UUID uuid) {
        long msb = uuid.getMostSignificantBits();
        long timestampMs = msb >>> 16; // top 48 bits
        return Instant.ofEpochMilli(timestampMs);
    }

    public static void main(String[] args) {
        // Anyone with the UUID can extract the creation time
        // UUID: 019234ab-cdef-7xxx-xxxx-xxxxxxxxxxxx
        // Timestamp: 2026-06-15T10:30:00Z
    }
}

This is harmless for an internal identifier but means UUIDv7 must never serve as a secret token.

Why is UUIDv7 not a secret or auth token?

Because part of the value is predictable time and only the tail is random, a UUIDv7 is not unguessable in the cryptographic sense.

JAVA
package com.bank.security;

import java.security.SecureRandom;
import java.util.Base64;

public class TokenService {

    // Wrong: UUIDv7 as a password-reset token
    // The timestamp is predictable, reducing entropy

    // Correct: fully random, cryptographically secure token
    public String generateSecureToken() {
        byte[] bytes = new byte[32]; // 256 bits
        new SecureRandom().nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }
}

When should you use UUIDv7 versus alternatives?

| Use Case | Recommended ID Type |
|---|---|
| Distributed primary key | UUIDv7 (locality + uniqueness) |
| Strict global ordering | Database sequence or logical clock |
| Session or reset token | 256-bit SecureRandom |
| Public API identifier | UUIDv7 (if time leak is acceptable) |
| Legal invoice number | Gapless database sequence |

How do you use UUIDv7 with JPA/Hibernate?

Rajesh maps the UUID as the entity id at National Bank of India.

JAVA
package com.bank.model;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.UUID;

@Entity
@Table(name = "transactions")
public class Transaction {

    @Id
    @Column(columnDefinition = "uuid")
    private UUID id;

    @Column(precision = 18, scale = 2)
    private BigDecimal amount;

    @PrePersist
    void generateId() {
        if (id == null) {
            // Use a UUIDv7 generator library
            // id = UUIDv7.generate();
        }
    }
}

What are common interview follow-up questions?

  • How does UUIDv7 compare to UUIDv4 for index performance? (v7 is time-ordered so inserts are sequential; v4 is random and causes page splits.)
  • Can you use UUIDv7 for pagination? (Yes, for rough cursor-based pagination, but not for strict ordering across nodes.)
  • What about UUIDv6? (Similar to v7 but reorders the fields of v1; v7 is the modern choice.)

Visual Overview

DIAGRAM
  UUIDv7 structure (128 bits):
  ┌──────────────────────────┬──────┬──────────────────────┐
  │ 48-bit Unix timestamp ms │ ver  │  74-bit random        │
  │ (creation time: visible) │ (7)  │  (entropy)            │
  └──────────────────────────┴──────┴──────────────────────┘

  Caveat 1: Clock skew across nodes
  Node A: 10:00:00.000 ──▶ UUID_A
  Node B: 10:00:00.003 ──▶ UUID_B
  If clocks disagree: UUID_A may sort before UUID_B even if B was first

  Caveat 2: Storage
  BINARY(16): 16 bytes ◄── compact
  CHAR(36):   36 bytes ◄── 2.25x larger, bloats every index

  Caveat 3: Time leakage
  UUID ──▶ extract top 48 bits ──▶ creation timestamp revealed
  Identifier: OK │ Secret/token: NOT OK

Key Takeaway

UUIDv7 is excellent for distributed primary keys because its time prefix gives B-tree locality and avoids the random-insert fragmentation of UUIDv4. But it has three caveats that cause real bugs if ignored.

First, ordering scope: the time prefix comes from each node's wall clock, so within one node the ids are reliably increasing, but across nodes clock skew makes the ordering only approximate. Never use UUIDv7 ordering for strict business sequencing like settlement order.

Second, storage: a UUID is 16 bytes as a native type or BINARY(16), but 36 bytes as CHAR(36). Since the primary key is embedded in every secondary index in InnoDB, the text form bloats the entire index footprint. Store compact, convert to text only at the display edge.

Third, confidentiality: the leading 48 bits are a readable creation timestamp, so a UUIDv7 reveals when a record was created. It is an identifier, never a secret, session token, or password-reset token. For those, use a 256-bit SecureRandom value.

At the National Bank of India, a settlement job once ordered cross-region transactions by their UUIDv7 ids and got the order wrong under clock skew. Rajesh fixed it by ordering on an explicit event-time column and reserving UUIDv7 as a compact, locality-friendly internal key.

Remember one sentence: UUIDv7 is approximately ordered, must be stored as 16-byte binary, and leaks creation time so it is an identifier, never a secret.

---

Share
Use ← → arrow keys to navigate