UUID Generator
v4 and v7 UUIDs in bulk, sortable ones that actually sort, plus a decoder for the ones you already have.
v4 is solved. v7 is the one worth getting right
There is nothing to win on a v4. It is one platform call —
crypto.randomUUID() — it has been non-experimental in every current
browser for years, and uuidgen is already installed on the machine you
are reading this on. This page generates one before you touch anything so that you
can copy it and leave.
Version 7 is different, and it is the reason this page exists. It was standardised in RFC 9562 in May 2024 — the document that obsoletes RFC 4122, which is worth saying because a good half of the results for this query still cite 4122 and its version list stops at 5. A v7 puts a 48-bit Unix millisecond timestamp in the leading bits, so the ids sort in creation order. That is the whole point: a random primary key scatters inserts across a B-tree and dirties a new page almost every time, while a time-ordered key appends to the right-hand edge of the index. "Should I use a UUID as a primary key" is really a question about that, and v7 is the answer that lets you say yes.
Most v7 generators are not sortable, and here is the measurement
RFC 9562 §5.7 pins only the first 48 bits. Everything after them is left to the implementation, and the obvious implementation fills the rest with random data. That sorts perfectly between milliseconds and not at all within one — and a batch generated in a loop is almost entirely within one.
Measured in Node 24.12 on the machine this page was built on, 10,000 ids in a tight loop, spanning 12 distinct milliseconds:
| Implementation | Ids | Consecutive pairs out of creation order |
|---|---|---|
v4, crypto.randomUUID | 1,000 | 509 of 999 — random, as expected |
| v7, random tail after the timestamp | 10,000 | 4,940 of 9,999 — 49.4% |
| v7, as generated above | 100,000 | 0 |
| v7, as generated above, all in one millisecond | 20,000 | 0, strictly increasing |
Half of a naive batch is out of order. A list of "sortable" ids that does not sort is worse than no v7 at all, which is why the generator above prints whether the batch it just produced is ascending instead of asking you to believe it.
The fix is two decisions, and they buy different things
RFC 9562 §6.2 method 2 says to put a monotonic counter in the 12 bits it calls
rand_a and to seed it randomly each new millisecond. That is right, and
it is not sufficient — 12 bits is 4,096 values, and both the seed width and the
overflow behaviour matter. Measured with those two as independent knobs, 10,000 ids
at roughly 830 per millisecond, five runs each:
| Counter seeded in | On overflow | Inversions per run |
|---|---|---|
| 0–4095 | wrap to 0 | 0, 2, 4, 3, 3 |
| 0–255 | wrap to 0 | 0, 0, 0, 0, 0 — but 4 at 20,000 in one millisecond |
| 0–4095 | borrow a millisecond | 0, 0, 0, 0, 0 |
| 0–255 | borrow a millisecond | 0, 0, 0, 0, 0 |
So borrowing a millisecond on overflow is what makes the ordering unconditional. Wrapping the counter back to zero inside a millisecond is the inversion, and a narrow seed only makes it rarer — at 20,000 ids in a single millisecond it still produced four. The same branch absorbs a clock that steps backwards, which NTP corrections and suspended VMs make a real event rather than a thought experiment: 150 ids across two backward steps measured zero inversions.
Seeding the counter in 0–255 rather than 0–4095 is a claim about the timestamp, not the order. Over 100,000 ids at 830 per millisecond, the wide seed borrowed 22 times and the narrow seed zero. Every borrow makes the embedded creation time one millisecond optimistic, so the narrow seed is what keeps a decoded v7 honest under load. The ceiling is bounded and worth stating rather than hiding: 200,000 ids stamped at a single wall-clock millisecond push the internal clock 50 ms ahead. The ids stay ordered and unique; their timestamp is up to 50 ms optimistic under a load no browser tab will ever produce.
The limit nobody hits until they do
crypto.getRandomValues refuses any request over 65,536 bytes and throws
QuotaExceededError. Measured identically in Node 24.12 and Chrome 148:
65,536 succeeds, 65,537 throws. At 16 bytes per id that is 4,096 UUIDs per
call, so a page offering "generate 10,000" that fills one buffer fails
outright — and the obvious fix, one call per id, is what makes a hand-rolled v7 an
order of magnitude slower than it needs to be. This page pools the randomness and
refills across that boundary, which is why the count control goes to 10,000 rather
than stopping at a number that hides the limit. Generating all 10,000 takes 2.1 ms;
rendering them is the only part slow enough to notice.
Generate the same UUIDs without this page
uuidgen # E5E01D84-AF5A-4C72-A13C-E4888CA33619 ← macOS prints uppercase uuidgen | tr 'A-Z' 'a-z' # what RFC 9562 §4 actually asks for python3 -c 'import uuid; print(uuid.uuid4())' python3 -c 'import uuid; print(uuid.uuid4().hex)' # 32 hex digits, no hyphens
crypto.randomUUID() // Needs a secure context: https, or localhost. Not available on a plain-http origin. // There is no library worth adding for this one.
let lastMs = -1, counter = 0;
function uuidv7(now = Date.now()) {
const b = crypto.getRandomValues(new Uint8Array(16));
if (now > lastMs) { // new millisecond: reseed the counter, but only in 0..255,
lastMs = now; // so ~3,840 increments remain before it can overflow
counter = b[7];
} else if (++counter > 0x0fff) {
lastMs++; // overflowed: borrow a millisecond rather than wrap to 0.
counter = b[7]; // this branch also absorbs a clock that steps backwards
}
b[0] = Math.floor(lastMs / 2 ** 40) % 256; // 48-bit big-endian timestamp.
b[1] = Math.floor(lastMs / 2 ** 32) % 256; // note % and not & 0xff: bitwise
b[2] = Math.floor(lastMs / 2 ** 24) % 256; // operators coerce to int32 and would
b[3] = Math.floor(lastMs / 2 ** 16) % 256; // silently give you the wrong byte
b[4] = Math.floor(lastMs / 2 ** 8) % 256;
b[5] = lastMs % 256;
b[6] = 0x70 | ((counter >>> 8) & 0x0f); // version 7 + counter high nibble
b[7] = counter & 0xff;
b[8] = 0x80 | (b[8] & 0x3f); // variant 10xx
return [...b].map((x) => x.toString(16).padStart(2, '0')).join('')
.replace(/^(.{8})(.{4})(.{4})(.{4})/, '$1-$2-$3-$4-');
}
That is the generator this page runs, and it is deliberately the one published here: handing you the fifteen-line version would hand you the bug the rest of this page is about. It measured zero inversions over 100,000 ids and over 20,000 forced into a single millisecond.
-- v4, built in since Postgres 13. No extension needed. CREATE TABLE orders (id uuid PRIMARY KEY DEFAULT gen_random_uuid()); -- v7, built in since Postgres 18. This is the one you want for a primary key. CREATE TABLE orders (id uuid PRIMARY KEY DEFAULT uuidv7()); -- On Postgres 17 or older, generate v7 in the application and insert it, -- or store the timestamp separately and index that instead.
Reading a UUID you already have
Every generator makes UUIDs; almost none will tell you what the one in your log line is. The decoder above reads the version nibble and the variant field, and pulls the timestamp out of the three versions that carry one — which is less obvious than it sounds, because they do not agree on an epoch or a layout. A v7 counts milliseconds from 1970. A v1 counts 100-nanosecond intervals from 1582-10-15, the date the Gregorian calendar was adopted, and splits the field into low, mid and high groups in that order. A v6 carries the same value reordered so that the bytes sort. Read a v6 as though it were a v1 and you get a wrong date that still looks like a date, which is worse than refusing to answer, so the two are decoded separately here and the 1582 offset is pinned by a test.
The decoder accepts what people actually paste: uppercase, the braced
{…} form that C# and the Windows registry use, a
urn:uuid: prefix, and 32 hex digits with no hyphens at all — which is what
a CHAR(32) column usually holds. It says which of those it accepted rather
than normalising silently. When it cannot read something it says why in specific terms:
the digit count, the offending character and its position, or the hyphen grouping —
never just "invalid UUID".
For a v7, the decoded creation time links through to the timestamp tool, because a v7's embedded value is a Unix millisecond count and "what time is that in my zone" is that page's entire subject. If you are shortening ids for a URL, the base64url row is the 16 bytes in 22 characters instead of 36 — that transform is base64, and the two pages meet there.
What this page does not do
It does not generate v1 or v6. A v1 embeds the MAC address of the machine that made it, which a browser cannot see, so every web implementation fakes it with random bits and still calls the result a v1 — a lie in the output. Both decode here; neither is generated. It also does not generate v3 or v5: those are name-based, deterministic from a namespace and a name, and computed with MD5 and SHA-1 — which is the hash tool's engine, not this one's. And it does not do ULID, NanoID, KSUID or Snowflake. Each is a different format with its own alphabet, and bolting them on as a fifth control would make this page worse at the thing it is for.
The specs this page implements
- RFC 9562 (May 2024) — Universally Unique IDentifiers (UUIDs). This is the current document and it obsoletes RFC 4122. §4 gives the string format and the lowercase-out, either-case-in rule; §5.4 and §5.7 define v4 and v7; §5.9 and §5.10 define the nil and max UUIDs; §6.2 covers monotonicity, including the counter method used above.
- RFC 4122 (2005) — obsolete, but still what most search results and a lot of library documentation cite. It defines versions 1 through 5 only, which is why pages built on it have no v7.
-
Web Cryptography API —
crypto.randomUUID()andcrypto.getRandomValues(), including the 65,536-byte quota measured above. Both require a secure context.
FAQ
Is it safe to put a v7 UUID in a URL?
It is safe in the sense that matters — a v7 is not guessable. Count the bits honestly, though, because this page does not emit the layout most descriptions assume: RFC 9562 leaves 74 bits after the timestamp, the version and the variant, and a generator that fills all of them randomly has 74 random bits. The one above spends 12 of them on the monotonic counter that makes the ids sort, so what it hands you is 62 random bits — and consecutive ids from one batch differ by exactly one in that counter. 62 bits is 4.6 x 10^18 possibilities per millisecond, so guessing is still not the attack; predicting the next id from one you were given is not either, because the 62 random bits are redrawn every time. But it is not opaque: the first 48 bits are the millisecond it was created, and anyone holding the id can read that. So /orders/01a0348e-bb9a-7087-959d-181567ea0296 tells a customer exactly when the order row was inserted, and two ids tell them how far apart two events were. Whether that matters is a product question, not a security one. If it does, use v4 for anything public-facing and keep v7 for the primary key, which is where its sortability was earning its keep anyway.
What are the odds two v4 UUIDs collide?
A v4 has 122 random bits, not 128 — four go to the version nibble and two to the variant. The birthday bound puts a 50% chance of one collision at about 2.7 x 10^18 ids, which is roughly 2^61, and a one-in-a-billion chance at about 1.0 x 10^14 ids. Put concretely: generating 860 million UUIDs every second for a hundred years gets you to a coin flip. That is the honest answer, and it is more useful than "never" — because it tells you the assumption actually being made, which is that your random number generator is sound. crypto.getRandomValues is; Math.random() is not, and a v4 built on Math.random() is the collision you will actually see.
I generated v7 UUIDs somewhere else and they do not sort. Why?
Almost certainly because that generator randomises the bits after the timestamp and nothing else. RFC 9562 §5.7 only fixes the first 48 bits as a millisecond; what follows is left to the implementation, and the obvious implementation fills it with random data. That sorts correctly between milliseconds and randomly inside one. Measured here: 10,000 ids generated in a tight loop spanned 12 distinct milliseconds, and 4,940 of the 9,999 consecutive pairs — 49.4% — were out of creation order. The fix is a monotonic counter in the 12 bits RFC 9562 calls rand_a, plus a rule for what to do when it overflows. Both are in the snippet above, and both are needed: a counter that wraps back to zero inside a millisecond reintroduces the inversion it was added to remove.
Does v7 have a year 2038 problem?
No. The 2038 problem is a signed 32-bit count of seconds. A v7 carries an unsigned 48-bit count of milliseconds, which runs out on +010889-08-02T05:31:50.655Z — measured by decoding a UUID whose timestamp field is all ones. It has a year 10889 problem. Worth knowing for the opposite reason people usually ask: because the field is unsigned and starts at the Unix epoch, a v7 cannot represent any time before 1970 at all, which is why a v7 with a plausible-looking date from the 1960s is a v7 someone fabricated.
What is 00000000-0000-0000-0000-000000000000?
The nil UUID, and it is a defined value rather than an error — RFC 9562 §5.9 gives it a name. In practice it is what you get when a column defaulted, a struct was zero-initialised, or a parser failed and returned a zero value instead of raising. This page decodes it rather than rejecting it, because a decoder that refuses the nil UUID is wrong and because seeing it named is usually the answer to why a foreign key does not match anything. Its counterpart, ffffffff-ffff-ffff-ffff-ffffffffffff, is the max UUID (§5.10), normally used as a sentinel at the top of a range scan.
Why does uuidgen on macOS print uppercase when the spec says lowercase?
RFC 9562 §4 says a UUID should be emitted in lowercase, but it also says implementations must accept either case on input — so uppercase output is a wart rather than a violation, and macOS uuidgen has printed it that way for long enough that changing it would break scripts. Verified on this machine: uuidgen returned E5E01D84-AF5A-4C72-A13C-E4888CA33619. It matters exactly once, when something compares UUIDs as strings instead of as bytes: Postgres, for instance, stores a uuid column as 16 bytes and does not care, but a string comparison in application code does. Paste an uppercase id into the decoder above and it will normalise it and say that it did.
Nothing on this page asks about where your input goes, because a UUID you generated is not sensitive and the question is answered the same way for every route here: the browser is instructed to block every request this page could make, and you can read the header yourself.