UUID
uuidv4 for identifiers that reveal nothing, uuidv7 for identifiers that sort by creation time, and a generator when ordering has to be strict.
Note
ELI5. A UUID is a 128-bit identifier written as 36 characters, made so that two machines can each invent one and never collide. Version 4 is entirely random. Version 7 puts a millisecond timestamp in the front and randomness behind it, so sorting the strings sorts the records by when they were created — which is what databases want from a primary key. Reach for v7 by default. Reach for v4 when the creation time is itself something you would rather not publish.
import { uuidv4, uuidv7, secureUUID } from "unsecure/uuid";
uuidv4(); // "ec4fba09-1ab4-4917-a29d-6fe7ab0e0f70"
uuidv7(); // "01a076ba-1e53-7411-8518-84905f353e3d"
secureUUID(); // the same function as uuidv7, under a version-agnostic nameAll of it is backed by crypto.getRandomValues and RFC 9562.
#Which one to use
| What you need | Use |
|---|---|
| A random identifier, version not part of the contract | secureUUID() |
| A sortable database key, scattered calls | uuidv7() |
| A sortable database key with strictly increasing output | createUUIDv7Generator() |
| To backfill historical events out of order | uuidv7(date) or the generator |
| An identifier that leaks nothing, not even a timestamp | uuidv4() |
| The creation time of an existing v7 | uuidv7Timestamp() |
| To check a string really is a canonical v4 or v7 | isUUIDv4() / isUUIDv7() |
#Signatures
function uuidv4(): string;
function uuidv7(timestamp?: Date | number): string;
const secureUUID: typeof uuidv7;
interface UUIDv7Generator {
next(timestamp?: Date | number): string;
}
function createUUIDv7Generator(): UUIDv7Generator;
function uuidv7Timestamp(uuid: string): number;
function isUUIDv4(value: unknown): value is string;
function isUUIDv7(value: unknown): value is string;#Embedding a timestamp
uuidv7() and gen.next() take an optional Date or Unix-millisecond number in place of Date.now(). Useful for tests, backfills and replays.
uuidv7(new Date("2020-01-01")); // "016f5e66-e800-778d-b4bc-ddd693ea9f20"
uuidv7(1_609_459_200_000); // "0176bb3e-7000-7831-a29f-5be8dd047233"Fractional milliseconds are floored. The value must be finite, non-negative and within [0, 2^48 - 1], or it is OUT_OF_RANGE. Anything that is not a Date or a number is INVALID_TYPE.
#Reading the timestamp back
import { isUUIDv7, uuidv7Timestamp } from "unsecure/uuid";
const id = uuidv7(new Date("2020-01-01T00:00:00Z"));
uuidv7Timestamp(id); // 1577836800000
new Date(uuidv7Timestamp(id)).toISOString(); // "2020-01-01T00:00:00.000Z"Only a v7 carries a timestamp. A v4's first 48 bits are random, so uuidv7Timestamp throws MALFORMED for one. Gate the call:
if (isUUIDv7(input)) {
const created = new Date(uuidv7Timestamp(input));
}The guards are case-insensitive and check the format, the version nibble and the variant bits.
#The monotonic generator
uuidv7() fills its random fields with pure random bits, so two UUIDs made in the same millisecond may sort in either order. When you need the stronger guarantee — sequential inserts landing on the same index page — use the generator.
import { createUUIDv7Generator } from "unsecure/uuid";
const gen = createUUIDv7Generator();
gen.next(); // "01a076ba-1e53-77f8-9627-b0e8f4e20d0a"
gen.next(); // "01a076ba-1e53-77f9-89f0-1b72b99c26a5"
gen.next(); // "01a076ba-1e53-77fa-9109-7dea2727cf00"With no argument every emitted UUID sorts after the previous one, through same-millisecond bursts, counter overflow and clock regressions.
#How it holds
The generator uses a dual clock. The 12-bit rand_a field becomes a monotonic counter (RFC 9562 §6.2 Method 3), and both the counter and its reference timestamp are driven only by Date.now(). A caller-supplied timestamp controls the embedded timestamp field of the output and nothing else.
- Seed. On each new wall-clock millisecond the counter is reseeded to a random value in
[0, 0x7ff], the lower half of its range. That leaves at least 2048 increments of headroom. - Overflow. Past 4095 UUIDs in one millisecond, the internal reference advances by 1 ms and the counter reseeds. Uniqueness holds.
- Clock regression. If
Date.now()goes backwards — an NTP adjustment, a VM pause — the reference is held and the counter keeps incrementing. - Caller timestamps do not perturb state.
gen.next(pastTs)embedspastTsverbatim while the counter still advances from the wall clock, which is what makes out-of-order backfills safe. - Scope is one process. Two generators, or two processes, share the counter field but stay distinguishable through the 62 random bits of
rand_b.
A next() call that throws does not mutate the generator's state.
#The v7 layout
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- 48-bit
unix_ts_ms, big-endian, milliseconds since the epoch. Good through roughly the year 10,895. - A 4-bit version nibble,
0111. - 12-bit
rand_a: random inuuidv7(), the counter in the generator. - A 2-bit variant,
10. - 62-bit
rand_b, always random.
#Errors
| Cause | code |
|---|---|
A timestamp that is not a Date or a number | INVALID_TYPE |
A timestamp outside [0, 2^48 - 1], or not finite | OUT_OF_RANGE |
uuidv7Timestamp on a string that is not a canonical v7 | MALFORMED |
uuidv7Timestamp on a value that is not a string | INVALID_TYPE |
#Pitfall: assuming uuidv7() is strictly monotonic
Two UUIDs from the same millisecond may appear in either order. Across different milliseconds the ordering is guaranteed. Use the generator when you need more than that.
#Pitfall: mixing caller timestamps and call order
The embedded timestamp dominates lexical order, so mixing next() and next(pastTs) gives UUIDs that sort by their embedded time, not by when you called.
const a = gen.next(); // embeds Date.now()
const b = gen.next(new Date("2020-01-01")); // b sorts before aThat is usually what you want for a database key. If you need "latest inserted sorts last", feed ascending timestamps or omit the argument. Two UUIDs carrying the same embedded timestamp sort by their counter only when both calls fall in the same wall-clock millisecond of this process; a pair straddling a boundary sorts in either order. They are still unique.
#Pitfall: reading sub-millisecond precision
The embedded timestamp is millisecond precision by construction. uuidv7Timestamp() cannot tell you anything finer. If ordering within a millisecond matters, use the generator — its counter encodes order, not time.
#Note on uuidv4()
This is not crypto.randomUUID(). The randomness source is the same, but the version and variant bits are set explicitly so the implementation is self-contained and does not shift if a runtime changes its shortcut.