CONCEPT Cited by 2 sources
NanoID¶
A NanoID is a URL-safe random string identifier (Andrey Sitnik, 2017) designed as a compact, collision- resistant alternative to UUIDs. Default NanoIDs are 21-character strings drawn from a 64-character URL- safe alphabet, giving 126 bits of entropy — essentially collision-free at typical generation rates. Unlike UUIDs, ULIDs, or Snowflake IDs, NanoIDs carry no timestamp — the bits are pure randomness, chosen for compactness and collision resistance rather than temporal ordering. (Primary source: sources/2026-04-21-planetscale-why-we-chose-nanoids-for-planetscales-api, with sibling framing from sources/2026-04-21-planetscale-the-problem-with-using-a-uuid-primary-key-in-mysql.)
First-party wiki datum: PlanetScale uses NanoIDs¶
From PlanetScale's 2022-03-29 canonical disclosure post (Source: sources/2026-04-21-planetscale-why-we-chose-nanoids-for-planetscales-api):
We decided that we wanted our IDs to be:
- Shorter than a UUID
- Easy to select with double clicking
- Low chance of collisions
- Easy to generate in multiple programming languages (we use Ruby and Go on our backend)
This led us to NanoID, which accomplishes exactly that.
— sources/2026-04-21-planetscale-why-we-chose-nanoids-for-planetscales-api
Four explicit selection criteria frame the choice: compactness, double-click-selectability, collision resistance, and cross-language generator ecosystem. The 2024 UUID-PK post forward-references this one with "(NanoIDs, which we use at PlanetScale)" — the 2022 post is where the decision is motivated and the mechanism disclosed.
PlanetScale's specific parameterisation¶
- Alphabet:
0123456789abcdefghijklmnopqrstuvwxyz— 36 characters (base-36 lowercase alphanumeric, no hyphens, no underscores, no uppercase). Narrower than NanoID's default 64-character URL-safe alphabet. - Length: 12 characters.
- Entropy: 12 × log2(36) ≈ 62 bits per ID.
- Collision budget (per post): "1% probability of a collision in the next ~35 years if we are generating 1,000 IDs per hour."
The narrower-alphabet choice trades entropy density for
double-click-selectability — base-36 alphanumerics
are selected as one word by browsers and terminals,
unlike NanoID-default's - and _.
Sample¶
Shorter than the canonical 21-character default — NanoID length is caller-configurable. The PlanetScale sample is 12 characters; shorter IDs trade collision resistance for compactness.
Alphabet¶
Default 64-character URL-safe alphabet:
All URL-safe. No ambiguous characters to filter out —
unlike Crockford base32 (used by ULID) which removes I,
L, O, U.
Collision resistance¶
At 21 characters × 6 bits/character = 126 bits of entropy (UUIDv4 is 122 bits after fixed version/variant bits — NanoID-21 is actually stronger). Collision probability for 10^9 IDs generated is ~10^-22.
At 12 characters (like the PlanetScale sample): 12 × 6 = 72 bits. Collision probability for 10^6 IDs is ~10^-10 — still effectively zero for per-tenant / per- resource namespaces but noticeably weaker than default.
Why use NanoIDs instead of UUIDs¶
- Compact — 21 characters vs UUID's 36. Saves 15 bytes per stored ID in string form.
- URL-safe out of the box — no hyphens, no equals signs, no encoding for URLs.
- Configurable length — 8, 12, 16, 21+ depending on namespace size and aesthetic preference.
- No hyphen noise — URLs look cleaner:
/api/v1/deploy-requests/kw2c0khavhqlvs/api/v1/deploy-requests/550e8400-e29b-41d4-a716-446655440000. - Strong PRNG required — most libraries use
crypto-grade randomness by default, unlike UUIDv4
libraries that sometimes fall back to
Math.random().
Why NOT use NanoIDs as a primary key¶
NanoIDs are not time-ordered. They have no timestamp field — the bits are pure randomness. As a clustered-index primary key, they trigger the full concepts/uuid-primary-key-antipattern: - Unpredictable insert path → cache miss per insert. - Write amplification via scattered splits. - Range scans fan out across non-adjacent leaves. - ~50% page fill instead of 94%.
PlanetScale's own choice is a deliberate one: NanoIDs
are external API identifiers, not clustered-index
primary keys. Internally, the underlying Vitess/MySQL
rows likely use BIGINT AUTO_INCREMENT clustered PKs
with the NanoID stored as a unique secondary index.
Typical use pattern¶
CREATE TABLE deploy_request (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- internal PK (sequential)
nanoid CHAR(12) NOT NULL UNIQUE, -- external API ID
-- other columns ...
);
The internal PK gets clustered-index locality; the
secondary-index lookup on nanoid costs one extra
B+tree walk but provides the opaque external ID.
See patterns/sequential-primary-key for the general "UUID/NanoID as external ID, BIGINT as internal PK" pattern.
vs UUIDs, ULIDs, Snowflakes¶
| Property | NanoID-21 | UUIDv4 | UUIDv7 | ULID | Snowflake |
|---|---|---|---|---|---|
| String length | 21 | 36 | 36 | 26 | ~19 (base-10) |
| Bit width | 126 | 128 | 128 | 128 | 64 |
| Time-ordered | No | No | Yes | Yes | Yes |
| URL-safe default | Yes | No (hyphens) | No (hyphens) | Yes | Yes |
| Configurable length | Yes | No | No | No | No |
| Standardised | No | Yes (RFC) | Yes (RFC) | No (de facto) | No |
| B+tree PK locality | Bad | Bad | Good | Good | Good |
NanoID trades time-ordering for compactness + URL- friendliness. Only family where string length is a design knob.
Caveats¶
- No timestamp recoverability. Sorting or filtering by "when was this ID generated" is impossible — no timestamp field to decode.
- Configurable length is a footgun. Reducing the length below the default 21 characters weakens collision resistance rapidly. At 8 chars × 6 bits = 48 bits, collision probability passes 1% at just 2M IDs. Audit the length against the expected cardinality.
- Not a standard. NanoID is specified in the
ai/nanoidGitHub repository; no RFC, no IANA registry, no IETF standardisation. - PRNG quality varies across libraries. A NanoID
library that uses a non-cryptographic PRNG (e.g.
Math.random()in old JavaScript) is vulnerable to prediction. Audit the library. - Harder to eyeball. Unlike ULID's timestamp prefix, NanoIDs are pure random noise. Debugging logs requires a separate timestamp column.
- Alphabet conflict with URL encoding in edge cases.
The default
-and_characters are URL-safe per RFC 3986 but some naive URL-parsing code still percent-encodes them.
Seen in¶
- sources/2026-04-21-planetscale-why-we-chose-nanoids-for-planetscales-api
— Mike Coutermarsh (PlanetScale, 2022-03-29) publishes
the primary-source canonical disclosure of why
PlanetScale picked NanoID. Four selection criteria
("shorter than UUID, easy to double-click-select, low
collision chance, easy to generate in multiple languages
— we use Ruby and Go"), specific 12-char × base-36
parameterisation, full
PublicIdGeneratorRails concern - Go
publicidpackage,CREATE TABLE userschema withBIGINT AUTO_INCREMENT PRIMARY KEY+public_id VARCHAR(12) UNIQUE KEY. Canonical public_id column + public-id-alongside-BIGINT-PK pattern. Sample values:izkpm55j334u,z2n60bhrj7e8,qoucu12dag1x. Introduces the double-click-selectability design axis as a first-class concern in API identifier format. - sources/2026-04-21-planetscale-the-problem-with-using-a-uuid-primary-key-in-mysql
— Brian Morrison II (PlanetScale, 2024-03-19) names
NanoIDs as the third UUID alternative and forward-
references the Coutermarsh post: "NanoIDs (which we
use at PlanetScale)" with sample value
kw2c0khavhql.
Related¶
- concepts/uuid-version-taxonomy
- concepts/snowflake-id — time-ordered 64-bit sibling
- concepts/ulid-identifier — time-ordered 128-bit sibling
- concepts/uuid-primary-key-antipattern — why NanoIDs shouldn't be clustered-index PKs
- concepts/public-id-column — NanoID's canonical column shape
- concepts/nanoid-collision-retry — bounded-retry correctness contract
- concepts/double-click-selectable-identifier — UX design axis NanoID can target or miss depending on alphabet choice
- patterns/public-id-alongside-bigint-pk — PlanetScale's NanoID-as-external-ID + BIGINT-as-PK pattern
- patterns/sequential-primary-key
- systems/planetscale
- systems/ruby-on-rails