Skip to content

CONCEPT

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: , with sibling framing from .)

First-party wiki datum: PlanetScale uses NanoIDs

From PlanetScale's 2022-03-29 canonical disclosure post (Source: ):

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.

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: 0123456789abcdefghijklmnopqrstuvwxyz36 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

kw2c0khavhql

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:

A-Z a-z 0-9 _ -

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

  1. Compact — 21 characters vs UUID's 36. Saves 15 bytes per stored ID in string form.
  2. URL-safe out of the box — no hyphens, no equals signs, no encoding for URLs.
  3. Configurable length — 8, 12, 16, 21+ depending on namespace size and aesthetic preference.
  4. No hyphen noise — URLs look cleaner: /api/v1/deploy-requests/kw2c0khavhql vs /api/v1/deploy-requests/550e8400-e29b-41d4-a716-446655440000.
  5. 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/nanoid GitHub 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

  • — 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 PublicIdGenerator Rails concern
  • Go publicid package, CREATE TABLE user schema with BIGINT 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.
  • — 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.
Last updated · 542 distilled / 1,571 read