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 name

All of it is backed by crypto.getRandomValues and RFC 9562.

#Which one to use

What you needUse
A random identifier, version not part of the contractsecureUUID()
A sortable database key, scattered callsuuidv7()
A sortable database key with strictly increasing outputcreateUUIDv7Generator()
To backfill historical events out of orderuuidv7(date) or the generator
An identifier that leaks nothing, not even a timestampuuidv4()
The creation time of an existing v7uuidv7Timestamp()
To check a string really is a canonical v4 or v7isUUIDv4() / 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) embeds pastTs verbatim 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 in uuidv7(), the counter in the generator.
  • A 2-bit variant, 10.
  • 62-bit rand_b, always random.

#Errors

Causecode
A timestamp that is not a Date or a numberINVALID_TYPE
A timestamp outside [0, 2^48 - 1], or not finiteOUT_OF_RANGE
uuidv7Timestamp on a string that is not a canonical v7MALFORMED
uuidv7Timestamp on a value that is not a stringINVALID_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 a

That 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.