Generators

UUID v4 vs v7: Choosing Database IDs Without Guesswork

A practical guide to UUIDv4 and UUIDv7: timestamps, ordering, database tradeoffs, collision basics, and when a different ID format fits better.

Tooleras22 min read4,123 words

UUID versions solve different identifier problems. UUIDv4 is random and opaque; UUIDv7 carries a creation-time signal that can improve time-oriented ordering. Neither is automatically best for every database, API, or security boundary.

This guide separates the standard's guarantees from generator-specific behavior. The UUID Generator on this site creates and inspects UUIDv4 and UUIDv7 locally.

What a UUID actually is, in 30 seconds

A UUID is 128 bits. That's 16 bytes, or 32 hexadecimal digits, conventionally grouped 8-4-4-4-12 with hyphens:

019297c9-2c7a-7a62-b1d4-4b3c5a8e0f91

Two of those hex digits tell you what kind of UUID you're looking at. The version is the first character of the third group — 7 in the example above. The variant is the first character of the fourth group — b here, which (along with 8, 9, and a) means this UUID follows the IETF standard (RFC 9562 for new ones, RFC 4122 for legacy).

Once you know where to look, reading a UUID is trivial:

019297c9-2c7a-7a62-b1d4-4b3c5a8e0f91
                │           │
                └── version │
                            └── variant

Version 4 means it's fully random. Version 7 means the leading bits are a timestamp. Version 5 means it's a deterministic hash. Version 1 is the old MAC-and-timestamp format. The rest of the bits are either random, encoded data, or a combination.

The format is agreed upon globally. Microsoft calls the same thing a GUID. Databases store them as a 16-byte binary column. Languages expose them as 32-char hex strings. Internally: always 128 bits.

Version by version: what's actually in each one

Eight versions exist in the spec. Most developers use two of them. The others are worth knowing because they solve problems you'll eventually hit.

Version 1 — timestamp + node

UUIDv1 was the original design from RFC 4122. The bits are structured as a 60-bit timestamp (100-nanosecond intervals since October 15, 1582 — yes, really, the date of the Gregorian calendar reform), a 14-bit clock sequence, and a 48-bit node identifier that was originally supposed to be the machine's MAC address.

Two problems emerged. One: exposing a MAC address leaks information about what hardware generated the ID, which is a privacy issue. Two: if you generated UUIDs on a virtual machine or a container without a stable MAC, uniqueness assumptions broke. Modern implementations randomize the node field to defuse both problems, but at that point you've lost most of what made v1 distinct.

Don't start new projects with v1. If you want time-ordering, v7 is better in every way that matters. V1 persists in legacy codebases and occasionally in systems that predate v7.

Version 3 — name-based with MD5

UUIDv3 is deterministic. You feed it a namespace UUID and a name string, it hashes them together with MD5, and you get the same UUID every time for the same inputs. The point is idempotency — two independent systems can generate the same identifier for the same entity without coordinating.

The problem is MD5. It's been cryptographically broken since the mid-2000s. You can construct collisions deliberately. For a non-adversarial context (internal content addressing, migration key mapping) it's still fine because the risk is theoretical. For anything touching security, use v5 instead.

Version 4 — random

The workhorse. 122 of the 128 bits are random (6 bits are reserved for version and variant), drawn from whatever cryptographic random source your language provides — crypto.getRandomValues in the browser, crypto/rand in Go, secrets in Python, /dev/urandom on Linux.

V4 has been the default for fifteen years for a reason: it's simple, it needs no coordination, and the collision probability is absurdly low. It's the right choice for session tokens, request trace IDs, one-shot identifiers, and anything that isn't a database primary key. The only downside — the one that made v7 necessary — is that pure randomness is bad for B-tree indexes, which we'll get to.

Version 5 — name-based with SHA-1

Exactly like v3 but with SHA-1 instead of MD5. Still deterministic, still takes a namespace and a name. SHA-1 is also considered broken for cryptographic purposes but for UUID generation the attack models don't apply — we're not signing anything, just producing a 128-bit fingerprint of a name.

Use v5 when you need the same input to produce the same UUID across independent systems. Common cases:

  • Content addressing — hash a URL or file path into a stable ID
  • Idempotency keys — same operation parameters produce the same request ID
  • Migration — map legacy IDs to new UUIDs deterministically
  • Cross-system joins — two services derive the same UUID for the same business entity without a shared database

Our hash generator computes raw SHA-1 if you want to see the underlying hash before UUID formatting. Note that v5 truncates and modifies the hash to fit UUID format — the v5 output is not a direct SHA-1 of the input.

Version 6 — v1 reordered

UUIDv6 is v1 with the timestamp bits rearranged so the UUID sorts lexicographically in time order. It exists mostly for teams that had v1 values and wanted to switch to something sortable without rewriting everything.

V6 is a footnote. V7 does the same job with better entropy, a simpler bit layout, and no MAC address to worry about. Skip v6 unless you're specifically migrating from v1.

Version 7 — timestamp + random

The new default. Bit layout:

48 bits   4 bits   12 bits   2 bits   62 bits
┌───────┬────────┬─────────┬────────┬────────┐
│  ms   │ ver=7  │ rand_a  │ var=10 │ rand_b │
│ unix  │        │         │        │        │
└───────┴────────┴─────────┴────────┴────────┘

The first 48 bits are a Unix millisecond timestamp — the same format Date.now() returns in JavaScript. Then 4 bits for the version identifier, 12 bits in rand_a, 2 variant bits, and 62 bits in rand_b. RFC 9562 allows those 74 payload bits to be random data, or to include a sub-millisecond value and/or counter for additional ordering. Collision behavior and monotonicity therefore depend on the generator's implementation, not on the version label alone.

The timestamp goes in the most significant bits, which is the magic. When sorted as a 128-bit number, v7 UUIDs come out in roughly creation order. When stored as a database primary key, new rows append to the right side of the B-tree instead of scattering. You get the distributed-generation benefits of UUIDs with the insert-performance profile of auto-incrementing integers.

To inspect a v7, use the UUID Generator's Validate tab. It identifies version 7 and displays its embedded millisecond timestamp without sending the value to a server.

Version 8 — custom

V8 is a blank canvas. The spec defines the version field and the variant field; everything else is up to the implementer. It exists for teams that want RFC-compliant IDs with custom structure — maybe a tenant ID baked into the leading bytes, maybe a sharding prefix, maybe a specific timestamp format that doesn't match v7's layout.

Nobody should pick v8 unless they have a specific reason and know what they're doing. It's an escape hatch, not a default.

Collision probability, with numbers that mean something

The birthday paradox explains why you need fewer UUIDs to hit a collision than pure intuition suggests. With N bits of entropy, the collision threshold (50% chance of at least one duplicate) is roughly 2^(N/2) values — not 2^N. That's the square root, which is a much smaller number.

For UUIDv4 with 122 bits of entropy:

  • 50% collision probability at about 2^61 UUIDs = 2.3 × 10^18 values = 2.3 quintillion.
  • Generate one billion UUIDs per second nonstop, that's ~73 years to reach the threshold.
  • Generate one million UUIDs per second — which is more than most real applications sustain — that's ~73,000 years.

For UUIDv7 with 74 bits of random entropy:

  • 50% collision probability at about 2^37 UUIDs per millisecond.
  • The timestamp differentiates UUIDs generated in different milliseconds, so collisions only matter within the same millisecond.
  • You'd need to generate about 130 billion UUIDs in a single millisecond to hit 50% — physically impossible on current hardware.

The upshot: if you see duplicate UUIDs in production, it's always a broken random number generator, not actual statistical collision. The classic culprit is an app using Math.random() in JavaScript instead of crypto.getRandomValues(). Math.random() has maybe 32 useful bits of state depending on the engine, which reduces your effective entropy enough to make collisions genuinely possible. Every UUID library in every modern language uses cryptographic randomness by default. Roll your own and you're taking unnecessary risk.

UUIDv7 in depth

UUIDv7 places a 48-bit Unix timestamp in milliseconds in the most significant bits. The rest of its layout carries the version and variant markers plus a 74-bit payload area. RFC 9562 permits an implementation to fill that payload with random data or reserve some of it for a counter or additional clock precision.

That distinction matters. A v7 value usually sorts by its embedded millisecond, but strict order for values generated during the same millisecond is an implementation property. A generator that needs that guarantee must maintain suitable state and handle clock movement and counter rollover.

Tooleras can generate an ordered v7 batch within one generation action. It does not promise a process-wide monotonic sequence across separate clicks, browser tabs, or machines. RFC 9562 is the source of truth for the layout and monotonicity options.

The first 12 hexadecimal characters of a canonical v7 represent its Unix-millisecond timestamp. The UUID Generator's Validate tab can display that timestamp for an existing v7.

The database primary key question

This is the debate that drove v7's adoption, so it's worth going through carefully.

A B-tree index on a primary key stores rows sorted by the key value. When you insert a row, the database finds the right leaf page and writes the entry there. If the new key value sorts greater than every existing key, it lands on the rightmost page and appends cheaply — that page stays hot in memory, fills up, and eventually splits into two when full.

Random UUID values break this. Every UUIDv4 insert lands on a random leaf page. Pages get touched and dirtied unpredictably. The hot working set is effectively the entire index. Pages split more often because writes arrive out of order, and those splits leave the index fragmented with half-full pages. At small scale none of this matters; at millions of rows it matters somewhat; at hundreds of millions it can dominate your database's I/O profile.

UUIDv7 puts the timestamp first. New v7 values are numerically greater than recent v7 values, so inserts append to the right side of the B-tree just like sequential integers. The index stays sequential, fragmentation stops accumulating, and the hot working set reduces to the recent pages. Benchmarks from Credativ and Neon show insert throughput improvements of 2–10x on large tables when switching from v4 to v7.

Per-database support, as of 2026

Database support is version-specific. PostgreSQL 18 includes a native uuidv7() function; check your server's UUID function documentation before relying on it. Other engines may require application-side generation, a maintained extension, or a database-specific function.

The storage decision is separate from generation: binary UUID storage is compact, while text storage is easier to inspect. Benchmark the actual engine, index, workload, and driver you plan to use rather than assuming a version label guarantees an insertion-speed result.

When v4 is still fine

Not every use case needs v7. Session tokens, trace IDs, API request IDs, correlation IDs, short-lived cache keys — anything that isn't a primary key on a massive table — v4 is just fine. The index fragmentation argument only applies when the UUID is the clustered or primary index. A v4 on an unindexed column or as a secondary identifier costs nothing.

Alternatives worth knowing

UUIDs aren't the only game. A few alternatives solve specific problems better, and knowing when to reach for them separates a senior engineer's design from a tutorial's.

FormatSizeEncodingTime-sortedNotes
UUIDv4128 bitshexnothe classic; random
UUIDv7128 bitshexyesthe new default for PKs
ULID128 bitsCrockford base32yes26 chars, URL-safe, popular alternative to v7
NanoIDconfigurableURL-safe base64 variantno21 chars default; smaller = tradeoff
KSUID160 bitsbase62yes27 chars; 32-bit timestamp limits to year 2136
Snowflake64 bitsdecimal integeryesneeds worker ID registry
CUID2variablebase36nocollision-resistant client-side
TSID64 bitsinteger or base32yesfits in BIGINT; popular in Java ecosystems

A few worth calling out:

ULID (ulid/spec) was the most popular v7 alternative before v7 existed. Same 128-bit format, same time-sorting idea, but Crockford base32 instead of hex gives you 26 case-insensitive alphanumeric characters (no O/0, I/L confusion) that are URL-safe by default. For greenfield projects in 2026, v7 is usually the better choice because it's an IETF standard with native database support. ULID still wins if you want a shorter human-readable form — 01HE7VZJ3W5A9RWK8G9QEMJW5A reads more cleanly than a hex UUID.

NanoID (ai/nanoid) is different — not a standard, just a pragmatic 21-character URL-safe ID generator. 21 chars of the default alphabet gives ~126 bits of entropy, comparable to UUIDv4. It's the right pick for public-facing URLs where you want compact tokens that don't scream "database ID." If you're already using our random string generator for short unique tokens, NanoID formalizes that pattern.

Snowflake IDs fit in 64 bits, which matters if you really care about storage. They encode time + worker ID + sequence. The catch is that every worker needs a unique ID, which means a registry, which means coordination, which is the problem UUIDs were invented to avoid. Twitter created Snowflake for tweets; Discord uses it for message IDs. Use it when 64-bit storage actually matters and you have infrastructure for worker ID assignment. Most apps don't.

CUID2 (paralleldrive/cuid2) is a client-side generator that uses SipHash over per-fingerprint state to minimize collision risk in untrusted environments — good for client-generated IDs where you don't control the entropy source. It's also time-sortable and shorter than UUIDs. Less standard, but growing adoption in frontend-heavy apps.

When UUIDs are the wrong answer

This section is where most guides pretend UUIDs are always right. They aren't.

If you're building a single-instance app with a single database that you have no plans to shard, BIGSERIAL (Postgres) or AUTO_INCREMENT (MySQL) is usually the better choice. The reasons:

Size. An integer primary key is 4 or 8 bytes. A UUID is 16. Across a large table with several indexes, that doubles the space your indexes take in memory. Smaller index = more fits in cache = faster lookups.

Simplicity. Integer IDs are human-readable. You can cite "user 42" in a bug report. You can spot an off-by-one in a migration. UUIDs all look the same, so you end up copy-pasting them, which eats time.

Natural ordering. Auto-increment integers are trivially sortable. v7 gets you most of the way there but still costs a hex-to-bigint conversion if you want to sort numerically.

The argument for UUIDs is distribution. If multiple services might generate new records simultaneously, or if you want to mint an ID on the client before it hits the database, or if you plan to merge databases, UUIDs buy you those capabilities for the cost of the extra 8 bytes. But if you aren't doing any of those things, don't pay the cost. Auto-increment is the more boring, more reliable choice for plenty of real-world applications.

Exposing integer IDs in URLs is sometimes called out as a security risk — someone can increment /users/42 to /users/43 and see if that user exists. This is a IDOR vulnerability and the fix is authorization, not opaque IDs. UUIDs in URLs make the enumeration slightly harder, but any system that relies on unguessable IDs for security is broken. Auth is what protects resources; UUIDs are a distraction if you conflate the two.

Generating UUIDs in code

Every modern language has this solved. Don't write your own.

Node.js and the browser

// v4 — built into Node 14.17+ and all modern browsers
const id = crypto.randomUUID();
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"

// v7 — use the uuid package (v11+)
import { v7 as uuidv7 } from "uuid";
const id7 = uuidv7();

crypto.randomUUID() is the standard across the JS ecosystem. On the browser side it's available in all evergreen browsers since 2021. On Node it's been there since 14.17 (May 2021) and stabilized by 19. For v7, the uuid package added support in version 11 and handles the per-millisecond monotonicity correctly.

Python

import uuid

# v4 — standard library
uid = uuid.uuid4()
# UUID('f47ac10b-58cc-4372-a567-0e02b2c3d479')

# v7 — use uuid_utils until Python 3.14 adds uuid7 natively
from uuid_utils import uuid7
uid7 = uuid7()

Python 3.14 (release scheduled for October 2025) adds uuid.uuid7() to the stdlib. Until then, uuid_utils or uuid6-python are the established libraries.

Go

import "github.com/google/uuid"

// v4
id := uuid.New()

// v7
id7, err := uuid.NewV7()

The google/uuid package added v7 support in late 2024. It's the de facto standard in the Go ecosystem.

Rust

use uuid::Uuid;

// v4
let id = Uuid::new_v4();

// v7 — requires the "v7" feature in Cargo.toml
let id7 = Uuid::now_v7();

If you just need a few right now

The UUID Generator creates UUIDv4 and UUIDv7 locally in batches of up to 100. It can show canonical, uppercase, dash-free, or brace-wrapped display formats, export a batch as text, CSV, or JSON, and validate an existing UUID. For a v7 batch, strict ordering applies only within that one generation action.

Security notes

  • UUIDv4 is a reasonable identifier when creation time should not be exposed and the value is generated with a cryptographically secure random source. It is still an identifier, not an authorization check.
  • UUIDv7 exposes a creation-time signal. Consider v4 or another opaque identifier when that timing information is sensitive.
  • UUIDv1 and UUIDv6 have different privacy tradeoffs from v4 and v7; do not use a UUID version as a password hash or secret-derivation scheme.
  • Use a maintained server-side password-hashing library with Argon2id or bcrypt for passwords. Do not put production passwords into browser utilities.
  • Every resource endpoint still needs authorization. An unguessable-looking URL does not grant access.

See the OWASP Password Storage Cheat Sheet for password-storage guidance.

FAQ

What's the difference between UUID and GUID?

Same thing, different name. GUID is Microsoft's term, used across .NET, SQL Server, and Windows APIs. UUID is the IETF standard name used basically everywhere else. The 128-bit format is identical. The only practical difference is that SQL Server stores GUIDs in mixed-endian byte order, so the string representation of the same 16 bytes differs between SQL Server and most other systems — a cosmetic issue that sometimes surprises people debugging cross-system joins.

Can two UUIDs actually collide?

Mathematically yes; practically no. For UUIDv4, the 50% collision threshold is around 2.3 quintillion values. Generating a billion UUIDs per second, it takes about 73 years to reach that threshold. If you observe duplicate UUIDs in a real application, the explanation is always a broken random number generator — typically Math.random() used where crypto.getRandomValues() belongs — rather than real statistical collision.

Should I use v4 or v7 as a primary key?

V7 for anything heading into a primary key on a table you expect to grow. Time-ordering keeps B-tree indexes appending to the right side instead of fragmenting across random leaves. The performance difference is negligible at small scale and substantial at tens or hundreds of millions of rows. V4 is still fine for secondary columns, session tokens, and anything that isn't indexed on the clustered key.

How do I check if a UUID is a v7?

The first character of the third group (position 14 in the canonical string form) is the version digit. If it's 7, it's a v7. You can also parse the first 48 bits as a Unix millisecond timestamp; if the date makes sense, it's almost certainly v7 (or v6, which also front-loads time but is rare).

Is UUIDv7 the same as ULID?

Close but not identical. Both are 128 bits with a millisecond timestamp prefix and the rest random. ULID uses 48-bit timestamp + 80-bit random (more random bits than v7's 74). ULID uses Crockford base32 encoding instead of hex, giving you 26 case-insensitive alphanumeric characters. Functionally similar, but ULID predates the IETF standard, while v7 is now the formal RFC 9562 version.

Can I extract the timestamp from a UUIDv7?

Yes. Take the first 12 hexadecimal characters of the UUID, interpret as a hexadecimal number, and that's the Unix timestamp in milliseconds. Divide by 1000 for seconds, then convert to a human-readable date. It's a trivial operation and occasionally useful for debugging event ordering.

Does PostgreSQL support UUIDv7 natively?

As of PostgreSQL 18 (released late 2024), yes — the uuidv7() function is built in. Earlier versions (9.6 through 17) can use the pg_uuidv7 extension, or generate v7 values in application code and insert them as UUID typed columns. For new projects, upgrade to 18 if you can; for existing systems, the extension is a reasonable bridge.

Is UUIDv4 safe for use in session tokens?

Yes, provided your language's v4 generator uses cryptographic randomness. Node's crypto.randomUUID(), Python's uuid.uuid4(), Go's uuid.New(), and Rust's Uuid::new_v4() all do. 122 bits of random entropy is more than sufficient for session tokens — well beyond the threshold where brute-force becomes infeasible.

What's wrong with UUIDv1?

Two things. First, the node portion was originally the machine's MAC address, which leaks hardware identity. Second, if the node field isn't handled carefully in virtualized environments, the uniqueness guarantees can break. Modern implementations randomize the node field to fix both issues, but at that point v7 gives you time-ordering with better entropy and no legacy baggage. Use v7 for new work.

Should I migrate existing v4 primary keys to v7?

Usually no. The existing keys don't become "wrong" — they still work, they still uniquely identify rows. Migrating would require rewriting every foreign key, every cached reference, every log entry. The fragmentation damage on a v4 index is done; switching the default to v7 for new inserts stops the bleeding without the migration cost. Over time, the hot part of the index gets cleaner as it fills with v7 values.

Which UUID version should I use if I need deterministic IDs?

V5. Given the same namespace UUID and the same name string, v5 produces the same UUID every time. Useful for idempotency keys, content addressing, or cross-system joins without shared state. Avoid v3 — same idea but uses MD5, which is obsolete.

How big is a UUID in bytes?

128 bits = 16 bytes in binary form. As a string, the canonical form is 36 characters (32 hex digits plus 4 hyphens). Some storage formats strip the hyphens for 32 characters. Others add braces (common in .NET) for 38 characters.

Is there a "UUIDv9" or later coming?

Not formally. RFC 9562 defines v1 through v8, with v8 being the "custom" escape hatch. No work is publicly in progress on additional versions. If a new version is ever standardized, it will likely address a specific need that v7 doesn't cover.

How does UUIDv7 handle multiple UUIDs in the same millisecond?

RFC 9562 section 6.2 recommends — but doesn't mandate — a counter-based monotonicity scheme. Within a single millisecond, each new v7 UUID gets a counter value in the random_a bits, ensuring lexicographic ordering matches generation order. Mature libraries implement this correctly; rolling your own skips it and can produce out-of-order v7s under high throughput.

Can I use UUIDs in URLs?

Yes, and many apps do. UUIDs are URL-safe as long as you stick to the hyphen-separated hex form (all characters are URL-safe). The 36-character length is manageable for internal URLs but uglier than shorter alternatives like NanoID or Hashids if the URL is user-facing. For public URLs where aesthetics matter, consider NanoID (21 chars URL-safe) or a separate slug column.

One more thing

For a time-oriented database primary key, UUIDv7 is worth evaluating. For opaque values where timestamp disclosure is undesirable, UUIDv4 remains a good fit. Deterministic identifiers, sequential database keys, and human-readable public IDs are separate design problems; choose the format that matches the constraint instead of treating any UUID version as a universal default.

Practice with free tools

Practical browser tools with data-handling disclosures and reviewed limits on flagship workbenches.

Browse all tools →