Blog
3 min read

UUIDv7 Explained: Time-Ordered IDs and Why Postgres 18 Added uuidv7()

UUIDv7 is a UUID that starts with a millisecond timestamp, so new IDs sort in creation order. Why that makes database indexes faster than random UUIDv4, the format in detail, generating them in Postgres 18, Node.js and Python, what the timestamp leaks, and when to still use v4 or bigint.

UUIDv7 is a newer kind of UUID that puts a timestamp at the front. That one change means IDs generated later sort after IDs generated earlier — which makes them much friendlier to databases than the classic random UUIDv4.

It was standardised in RFC 9562 (2024), and PostgreSQL 18 added a built-in uuidv7() function.

What it looks like

0199c4e2-7b8a-7c3f-9d2e-4a6b8c1f0e57
└──────┬─────┘ └┬┘
  timestamp    version 7

The 128 bits break down as:

Bits Contents
48 Unix timestamp in milliseconds
4 Version (7)
12 Random (or sub-millisecond precision / counter)
2 Variant
62 Random

So it's still a normal UUID — the same uuid column type, the same string format — just with order built in.

Why order matters: indexes

Primary keys live in a B-tree index. With random UUIDv4, each new row's key lands at a random spot in the index, so:

  • Inserts touch pages all over the index, which hurts caching once the index is bigger than memory.
  • Pages split constantly, leaving the index bloated and fragmented.
  • Write-ahead log volume goes up.

With UUIDv7, new keys are always near the end of the index, like an auto-increment integer. Inserts stay in a few hot pages, and the index stays compact. On large tables the difference is significant. (Postgres Index Types, The Postgres WAL Explained)

Bonus: ORDER BY id roughly equals creation order, and IDs from different servers can be merged without coordination.

Generating UUIDv7

PostgreSQL 18+:

CREATE TABLE orders (
  id uuid PRIMARY KEY DEFAULT uuidv7(),
  created_at timestamptz NOT NULL DEFAULT now(),
  total_cents integer NOT NULL
);

SELECT uuid_extract_timestamp(id) FROM orders LIMIT 1;

On older Postgres, generate them in your application, or use an extension. (Postgres Major Version Upgrade)

Node.js: crypto.randomUUID() produces v4 only. Use the uuid package:

import { v7 as uuidv7 } from 'uuid'
const id = uuidv7()

Python 3.14+:

import uuid
uuid.uuid7()

For older Python versions, use a small library such as uuid6.

UUIDv7 vs v4 vs bigint

bigint identity UUIDv4 UUIDv7
Size 8 bytes 16 bytes 16 bytes
Index-friendly inserts Yes No Yes
Generate without the database No Yes Yes
Guessable / enumerable Yes (/orders/1001) No Hard (but time is visible)
Reveals creation time No No Yes

(UUID vs Auto-Increment)

The catch: it leaks a timestamp

Anyone who sees a UUIDv7 can read when it was created, to the millisecond. Usually harmless — but if creation time is sensitive (a user's signup time on a dating app, the timing of a medical record), use v4 for public identifiers. A common pattern is UUIDv7 as the internal primary key plus a separate random public ID where it matters.

UUIDv7 is also not a security token: its random part makes it hard to guess, but don't use any UUID as a secret or a password-reset token. (IDOR Explained)

Migrating

You don't have to rewrite existing IDs. Changing the column default from gen_random_uuid() to uuidv7() means new rows get v7 IDs while old v4 rows stay valid — they're the same type.

The summary

  • UUIDv7 = UUID with a millisecond timestamp first, so IDs sort by creation time.
  • Much better index behaviour than random v4 for primary keys.
  • Postgres 18 has uuidv7(); Node needs the uuid package; Python 3.14 has uuid.uuid7().
  • It reveals creation time — keep that in mind for public IDs.

EasySpawn runs a current PostgreSQL next to your app on each server, so features like uuidv7() are available without managing upgrades yourself. See how it works or join the waitlist.

Related: Primary Key vs Foreign Key · Design Your First Database · Postgres HOT Updates and Fillfactor · What Is PostgreSQL?

Keep reading