uuid.lol

What Your IDs Give Away

A UUID v4 from crypto.randomUUID(), or any modern runtime's CSPRNG, is 122 bits and is not guessable in any practical sense. Everything else here gives something away. UUID v1 and v6 carry a 60-bit timestamp and a node field that's sometimes a real MAC address. UUID v7 and ULID carry a 48-bit timestamp with no machine data. Any sequential ID tells an attacker exactly how many rows you have.

Which format should you use?

Use UUID v4, made with crypto.randomUUID(), when nothing about the ID should be guessable: session identifiers, password reset tokens, anything an attacker could act on directly. Generate a UUID v4 and it's ready to use as-is.

Use v7 or ULID instead when rows need to sort by creation time in the database. Both mean anyone who later sees the ID also learns, to the millisecond, roughly when the row was made. If you want something shorter with no structure at all, not even a timestamp, a Nano ID is fully random from end to end.

// Node.js 19+, and every current browser
const id = crypto.randomUUID();

// What randomUUID() is built on: 16 bytes from the platform CSPRNG
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);

Both come from the Web Crypto API: crypto.randomUUID() and crypto.getRandomValues() are both cryptographically secure, and that's the only property that determines whether an ID is guessable.

What does each ID format expose?

Every format below spends some of its bits on structure instead of randomness, and the table shows exactly what that buys and what it costs.

Format Timestamp Machine or sequence signal Bits you'd have to guess
UUID v4 None None 122 bits
UUID v7 Milliseconds since 1970-01-01 None 74 bits
UUID v1 100ns since 1582-10-15 Node field, sometimes a real MAC MAC + clock (14 bits)
UUID v6 100ns since 1582-10-15 Node field, sometimes a real MAC MAC + clock (14 bits)
ULID Milliseconds since 1970-01-01 None 80 bits
KSUID Seconds since 2014-05-13 None 128 bits
TypeID Milliseconds since 1970-01-01 None 74 bits
ObjectID Seconds since 1970-01-01 5-byte value, fixed per process 40 bits + counter
Snowflake Milliseconds since a custom epoch Worker id, assigned not random Nothing to guess
Nano ID None None 126 bits
Cuid2 None None Hashed, unstated

Every row comes from RFC 9562 for the UUID versions and each format's own spec for the rest. The format comparison has the full list and every field width.

What does a timestamp inside an ID tell someone?

It tells anyone holding the ID the exact moment, to the resolution that format stores, that the row was created. That's rarely dangerous on its own, but it composes: a public API that returns IDs in creation order lets an outsider infer your signup rate, a competitor's order volume, or how long one specific record has existed, from IDs they were already allowed to see.

Paste any v1, v6, v7, ULID, KSUID, ObjectID or Snowflake value into the box below and the timestamp comes out in the clear. The example already loaded is a UUID v7, 018f4e8a-1c2d-7a3b-9c4d-5e6f7a8b9c0d, and it decodes to 2024-05-06T15:33:45.133Z, accurate to the millisecond. No brute force is involved: the timestamp is 48 of the UUID's 128 bits, sitting in the open.

Version
7 (Unix time and random)
Variant
RFC 9562
Timestamp
2024-05-06T15:33:45.133ZLeaked
Random A
A3B
Random B
1C4D5E6F7A8B9C0D
Bytes
01 8F 4E 8A 1C 2D 7A 3B 9C 4D 5E 6F 7A 8B 9C 0D

If you want to know what's already leaking out of IDs you've already minted, decode a few of them before you change anything.

Does UUID v1 put a real MAC address in your IDs?

Sometimes, but RFC 9562 doesn't require it, and most current libraries don't do it. RFC 9562 section 5.1 defines the v1 node field as "an IEEE 802 MAC address, usually the host address," but section 6.10 permits an alternative: a 48-bit cryptographically random value with the least significant bit of the first octet forced to 1, the bit that marks a multicast address and can never appear on a real network card. The uuid npm package generates v1 IDs this way by default. Generate a UUID v1 here and it won't touch your network hardware either, unless you pass a node value in yourself.

UUID v6 carries the identical node field, just with the timestamp bytes reordered so the value sorts. Everything above about v1's node field applies to v6 unchanged.

When the node field is a genuine MAC, the first three bytes are a vendor prefix you can look up in IEEE's OUI registry, and the last three identify one specific network card, a hardware identifier leaking out of a database primary key. That's the exact case section 6.10 exists to avoid. If you're generating v1 IDs and don't know which behavior your library uses, check before you rely on either answer. What a MAC address is, and which IDs actually contain one goes deeper on the node field alone.

How many guesses would it take to land one of your IDs?

For every format whose bits come from a CSPRNG, brute forcing one specific value takes more guesses than anyone will run in a lifetime. At a million attempts a second, a generous rate for anyone hammering an endpoint, the 122 random bits in a UUID v4 keep one specific value safe for about 8.4 × 10²² years. Even UUID v7's smaller 74 random bits, what's left once you set the timestamp aside, take about 299 million years at that same rate.

FormatEntropyExpected guessesExpected time to one hit
UUID v4122 bits2.7 × 10368.4 × 1022 years
UUID v774 bits9.4 × 1021299 million years
ULID80 bits6.0 × 102319.2 billion years
Nano ID126 bits4.3 × 10371.3 × 1024 years
KSUID128 bits1.7 × 10385.4 × 1024 years
ObjectID40 bits + counter550,000,000,0006.36 days
SnowflakeCoordinated, not randomNot applicable: guessing is not the relevant attack, enumeration is.

Every ID here that is actually random takes more guesses than any real attacker can attempt: even at a billion attempts a second, the random bits in a UUID v4 or a ULID keep a specific value safe from brute force for longer than the universe has existed. A snowflake is the exception, and not because it is weak: its bits are assigned, not random, so there is nothing to guess. The attack that works against a snowflake is enumeration, walking the small, known range of worker and sequence values, not brute force.

Snowflake is the one exception in that table, and not because it's weak. Its bits are all assigned, a worker id and a sequence counter, not drawn from anywhere random, so there's nothing to brute force. The real attack against a Snowflake, or against any small incrementing id, is enumeration: request id 1, then 2, then 3, and read every row in order. Swapping that counter for a UUID v4 stops the attack entirely, because there's no adjacent value to walk to.

Is an unguessable ID the same as authorization?

No. An ID nobody can guess is not the same thing as a check that says this specific request is allowed to see this specific row.

A URL that grants access purely because it's hard to guess is a capability URL, and capability URLs leak through channels that have nothing to do with brute force. A Referer header carries the full URL to whatever site a user clicks to next. Browser history keeps it. A corporate proxy or a load balancer logs it. A chat app that unfurls a link preview fetches the URL itself and may cache what it finds. A screenshot, shared anywhere, carries whatever was in the address bar.

None of that requires guessing anything, someone just has to see the URL once. The fix is an authorization check on the server that confirms the requesting session owns that row, not a longer or more random ID. A 256-bit ID sitting in a Slack message is exactly as exposed as a 122-bit one.

Common questions

Is crypto.randomUUID() safe to use as a bearer token?

The randomness is fine: 122 CSPRNG bits is more than enough to resist guessing. The risk is the channel it travels over. Put a bearer token in an Authorization header or a cookie, not the URL path, so it never ends up in a Referer header or an access log.

Does a sequential integer id leak more than a UUID does?

Yes, and more directly than any timestamp does. An id that increments by one gives away your exact row count on every single request; a timestamp only ever gives away when one specific row was made.

Can I strip the timestamp out of a v7 or a ULID?

Not without it stopping being that format. If the creation time is sensitive, generate a UUID v4 for that record instead of trying to zero out or randomize part of a v7; anything downstream that decodes the value will assume the timestamp bits are real.