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 |
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 theuuidpackage; Python 3.14 hasuuid.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
Postgres LISTEN/NOTIFY: Real-Time Events Without Another Service
LISTEN and NOTIFY let Postgres send messages to connected clients when something happens — new orders, cache invalidation, live dashboards — without Redis or a message broker. How it works, a Node.js example with triggers, the 8000-byte payload limit, why notifications aren't durable, and the PgBouncer gotcha.
Optimistic vs Pessimistic Locking: Preventing Lost Updates
Two users edit the same record and one silently overwrites the other. Pessimistic locking (SELECT FOR UPDATE) blocks the second writer; optimistic locking (a version column) detects the conflict. How each works, code for both, atomic updates that avoid locks entirely, and how to choose.