Choosing a primary key format affects storage size, insert performance, index health and operational complexity. Three popular options are UUID v4, NanoID and Snowflake ID. Each solves uniqueness differently and carries distinct trade-offs when used as a database primary key.
Quick comparison
| Feature | UUID v4 | NanoID (default) | Snowflake ID |
|---|---|---|---|
| Size | 16 bytes (128-bit) | ~21 bytes (string) | 8 bytes (64-bit integer) |
| Sortable by time | No | No | Yes |
| Index locality | Poor (random) | Poor (random) | Excellent |
| Native DB type | Yes (uuid) | No (VARCHAR) | Yes (BIGINT) |
| Coordination needed | None | None | Worker / machine ID |
| Clock dependency | None | None | Yes |
| Collision resistance | Extremely high | Extremely high | Deterministic within worker |
| JS precision issue | None | None | Yes (must send as string) |
UUID v4 as primary key
UUID v4 is a 128-bit random value. It requires no coordination and works everywhere. PostgreSQL and recent MySQL versions provide a native UUID type, which stores the value in 16 bytes and supports efficient indexing operators.
The main drawback for primary keys is randomness. Inserts land at unpredictable positions in a B-tree, causing page splits and index fragmentation. On large, write-heavy tables this leads to larger indexes, more I/O and slower inserts over time. UUID v7 (time-ordered) largely solves this problem while remaining a standard UUID, but classic v4 does not.
NanoID as primary key
NanoID produces short, URL-safe random strings (default 21 characters). Collision resistance at the default length is comparable to UUID v4. It is convenient for client-side generation and public-facing identifiers.
As a primary key it shares the same random-order problem as UUID v4. In addition it must be stored as VARCHAR or TEXT, which uses more space than a native binary UUID or BIGINT and typically has slightly slower comparisons. NanoID is rarely the best choice for high-volume primary keys; it is better suited to secondary public tokens or short codes.
Snowflake ID as primary key
Snowflake ID (originated by Twitter) is a 64-bit integer composed of a timestamp, a worker/machine identifier and a sequence number. The usual layout is:
- 41 bits – milliseconds since a custom epoch (~69 years)
- 10 bits – worker ID (up to 1024 nodes)
- 12 bits – sequence (up to 4096 IDs per millisecond per worker)
Because the high bits contain the timestamp, IDs are roughly time-ordered. New inserts tend to append near the end of the B-tree, keeping indexes dense and insert performance high. The 8-byte size is half that of a UUID, which reduces index and foreign-key storage significantly at scale.
The cost is operational: every generator instance needs a unique worker ID, and the system depends on reasonably synchronized clocks. Clock rollback must be handled (usually by blocking until the clock catches up). In JavaScript environments the full 64-bit value exceeds the safe integer range, so Snowflake IDs should always be transmitted as strings in JSON.
Which one should you choose?
Prefer Snowflake ID when:
- You need the smallest possible primary key and high insert rates
- You already have (or can build) worker-ID allocation and clock monitoring
- IDs stay mostly internal and are not heavily exposed in public APIs
Prefer UUID (especially v7) when:
- You want zero coordination and maximum compatibility
- You value a formal standard and native database UUID support
- You prefer to avoid clock and machine-ID operational complexity
Prefer NanoID when:
- The identifier is mainly for public URLs, tokens or short codes
- Primary-key performance is not the dominant concern
- You need a compact, URL-safe string generated on the client
For most new projects that need a distributed, client-generatable primary key without operational overhead, UUID v7 is currently the most balanced default. Snowflake remains compelling when storage and raw insert throughput are critical and the team can manage the coordination requirements. NanoID is better kept for secondary, user-facing identifiers rather than the main primary key of large tables.
Generate examples
You can try all three formats in the browser (generation stays local):