Skip to content

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

Last updated · 470 distilled / 1,213 read