NeXID - Fast, lexicographically sortable unique IDs

March 8, 2026 · View on GitHub

npm version TypeScript License: MIT

A TypeScript implementation of globally unique identifiers that are lexicographically sortable, following the XID specification, originally inspired by Mongo Object ID algorithm. NeXID provides a high-performance solution for generating and working with XIDs across JavaScript runtimes.

Tip

Features

  • Lexicographically sortable: natural sorting in databases, binary searches, and indexes
  • Time-ordered: built-in chronological ordering (timestamp is the first component)
  • Compact: 20 characters vs 36 for UUIDs (44% smaller)
  • URL-safe: alphanumeric only (0-9 and a-v), no special characters to escape
  • Universal: works in Node.js, browsers, Deno, and edge runtimes
  • Fast: generates 10+ million IDs per second
  • Secure: uses platform-specific cryptographic random number generation
  • Adaptive: runtime environment detection with appropriate optimizations
  • Type-safe: branded types for compile-time safety

Installation

npm install nexid
yarn add nexid
pnpm add nexid

Requires Node.js 20 or >= 22.

Quick start

import NeXID from 'nexid';

// Universal entry point — async (auto-detects environment)
const nexid = await NeXID.init();

// Generate an XID object
const id = nexid.newId();
id.toString(); // "cv37img5tppgl4002kb0"

// High-throughput string-only generation (~30% faster)
const idString = nexid.fastId();

You can also resolve the environment separately, then init synchronously:

import { resolveEnvironment } from 'nexid';

const { init } = await resolveEnvironment();
const nexid = init();

Platform-specific entry points skip detection entirely and are synchronous:

import NeXID from 'nexid/deno'; // Deno
import NeXID from 'nexid/node'; // Node.js
import NeXID from 'nexid/web';  // Browser

// No await needed — init is synchronous
const nexid = NeXID.init();

API

init(options?)

Creates an XID generator. Returns Generator.API.

const nexid = NeXID.init({
  machineId: 'my-service-01',    // Override auto-detected machine ID
  processId: 42,                 // Override auto-detected process ID (0–65535)
  randomBytes: myCSPRNG,         // Custom (size: number) => Uint8Array
  allowInsecure: false,          // Allow non-cryptographic fallbacks (default: false)
  filterOffensiveWords: true,    // Reject IDs containing offensive words
  offensiveWords: ['myterm'],    // Additional words to block
});
OptionTypeDefaultDescription
machineIdstringAuto-detectedCustom machine identifier string (hashed before use)
processIdnumberAuto-detectedCustom process ID, masked to 16-bit
randomBytes(size: number) => Uint8ArrayAuto-detectedCustom CSPRNG implementation
allowInsecurebooleanfalseWhen false, throws if CSPRNG cannot be resolved
filterOffensiveWordsbooleanfalseReject IDs containing offensive word substrings
offensiveWordsstring[][]Additional words to block alongside the built-in list
maxFilterAttemptsnumber10Max attempts to find a clean ID when filtering is enabled

Generator API

Returned by init().

nexid.newId();            // Generate XID object (current time)
nexid.newId(new Date());  // Generate XID object with custom timestamp
nexid.fastId();           // Generate XID string directly (faster)

nexid.machineId;  // Hashed machine ID bytes (hex string)
nexid.processId;  // Process ID used by this instance
nexid.degraded;   // true if using insecure fallbacks

XID class

Immutable value object representing a 12-byte globally unique identifier.

Factory methods

import { XID } from 'nexid';

XID.fromBytes(bytes);  // Create from 12-byte Uint8Array
XID.fromString(str);   // Parse from 20-character string
XID.nilID();           // Create a nil (all-zero) ID

Instance properties

id.bytes;      // Readonly XIDBytes (12-byte Uint8Array)
id.time;       // Date extracted from timestamp component
id.machineId;  // Uint8Array (3-byte machine ID, copy-on-read)
id.processId;  // number (16-bit process ID)
id.counter;    // number (24-bit counter value)

Instance methods

id.toString();      // 20-character base32-hex string
id.toJSON();        // Same as toString() — JSON.stringify friendly
id.isNil();         // true if all bytes are zero
id.equals(other);   // true if identical bytes
id.compare(other);  // -1, 0, or 1 (lexicographic)

Helper functions

Standalone utility functions for working with XIDs. These are used internally by the XID class and available as a deep import:

// Internal module — not part of the public package exports
import { helpers } from 'nexid/core/helpers';

helpers.compare(a, b);       // Lexicographic XID comparison
helpers.equals(a, b);        // XID equality check
helpers.isNil(id);           // Check if XID is nil
helpers.sortIds(ids);        // Sort XID array chronologically
helpers.compareBytes(a, b);  // Lexicographic byte array comparison

Prefer the equivalent XID instance methods (id.compare(), id.equals(), id.isNil()) for typical usage.

Offensive word filter

Opt-in filtering rejects generated IDs that contain offensive substrings, retrying with a new counter value.

import NeXID, { BLOCKED_WORDS } from 'nexid/node';

// Use the built-in blocklist (57 curated offensive words)
const nexid = NeXID.init({ filterOffensiveWords: true });

// Extend the built-in blocklist with custom terms
const nexid2 = NeXID.init({
  filterOffensiveWords: true,
  offensiveWords: ['mycompany', 'badterm'],
});

BLOCKED_WORDS is exported from all entry points for inspection.

Exported types

import type { XIDBytes, XIDGenerator, XIDString } from 'nexid';

// XIDBytes       -- branded 12-byte Uint8Array
// XIDString      -- branded 20-character string
// XIDGenerator   -- alias for Generator.API

Architecture

XID structure

Each XID consists of 12 bytes (96 bits), encoded as 20 characters:

  ┌───────────────────────────────────────────────────────────────────────────┐
  │                         Binary structure (12 bytes)                       │
  ├────────────────────────┬──────────────────┬────────────┬──────────────────┤
  │        Timestamp       │    Machine ID    │ Process ID │      Counter     │
  │        (4 bytes)       │     (3 bytes)    │  (2 bytes) │     (3 bytes)    │
  └────────────────────────┴──────────────────┴────────────┴──────────────────┘

Timestamp (4 bytes)

32-bit unsigned integer representing seconds since Unix epoch. Positioned first in the byte sequence to enable lexicographical sorting by time.

Tradeoff: second-level precision instead of milliseconds allows for 136 years of timestamp space within 4 bytes.

Machine ID (3 bytes)

24-bit machine identifier derived from platform-specific sources, then hashed:

  • Node.js/Deno: OS host UUID (/etc/machine-id on Linux, IOPlatformUUID on macOS, registry MachineGuid on Windows), hashed with SHA-256
  • Browsers: localStorage-persisted random UUID via crypto.randomUUID(), with deterministic fingerprint fallback (navigator, screen, timezone), hashed with MurmurHash3
  • Edge: Adaptive generation based on available platform features

Values remain stable across restarts on the same machine.

Process ID (2 bytes)

16-bit process identifier:

  • Node.js: process.pid masked to 16-bit
  • Deno: Deno.pid masked to 16-bit
  • Browsers: Cryptographic random 16-bit value via crypto.getRandomValues()

Counter (3 bytes)

24-bit atomic counter for sub-second uniqueness:

  • Thread-safe via SharedArrayBuffer + Atomics (with WebAssembly and ArrayBuffer fallbacks)
  • Re-seeded with a fresh 24-bit CSPRNG value on each new second
  • 16,777,216 unique IDs per second per process
  • Automatic wrapping with 24-bit mask

Encoding

Base32-hex (0-9, a-v) encoding yields 20-character strings:

  • Direct byte-to-character mapping with no padding
  • Lexicographically preserves binary order
  • Implemented with lookup tables for performance

Runtime adaptability

The implementation detects its environment and applies appropriate strategies:

  • Server (Node.js, Deno): hardware identifiers, process IDs, native cryptography, SHA-256
  • Browser: localStorage persistence, fingerprinting fallback, Web Crypto API, MurmurHash3
  • Edge/Serverless: adapts to constrained environments with fallback mechanisms

Detected runtimes: Node.js, Browser, Web Worker, Service Worker, Deno, Bun, React Native, Electron (main + renderer), Edge Runtime.

System impact

Database operations

Lexicographical sortability enables database optimizations:

  • Index efficiency: B-tree indices perform optimally with ordered keys
  • Range queries: time-based queries function as simple index scans
  • Storage: 44% size reduction translates to storage savings at scale

Example range query:

-- Retrieving time-ordered data without timestamp columns
SELECT * FROM events
WHERE id >= 'cv37ijlxxxxxxxxxxxxxxx' -- Start timestamp
AND id <= 'cv37mogxxxxxxxxxxxxxxx'   -- End timestamp

Distributed systems

  • No coordination: no central ID service required
  • Horizontal scaling: services generate IDs independently without conflicts
  • Failure isolation: no dependency on external services
  • Global uniqueness: maintains uniqueness across geographic distribution

Performance

NeXID delivers high performance on par with or exceeding Node's native randomUUID:

ImplementationIDs/SecondTime sortableCollision resistanceURL-safeCoordination-freeCompact
hyperid53,243,635
NeXID.fastId()9,910,237
node randomUUID8,933,319
uuid v48,734,995
nanoid6,438,064
uuid v73,174,575
uuid v12,950,065
ksuid66,934
ulid48,760
cuid26,611

Benchmarks on Node.js v22 on Apple Silicon. Results may vary by environment.

Note on speed and security

For password hashing, slowness is intentional: attackers must brute-force a small input space (human-chosen passwords), so making each attempt expensive is the defense (that's why bcrypt/argon2 exist).

For unique IDs, security comes from entropy (randomness). If an ID has 128 bits of cryptographic randomness:

  • An attacker doesn't need your generator, they can enumerate candidates independently at any speed they want
  • The search space is 21282^{128} regardless of how fast you can generate IDs
  • Collision resistance is a function of bit-length (birthday bound), not generation throughput
  • There's no "entropy-hiding" to break, the output is the random value

Note on SubtleCrypto() vs. MurmurHash3-32

The machine ID hash compresses an identifier like a hostname or browser fingerprint into 3 bytes with uniform distribution. With only 24 bits of output (16.7M possible values), the cryptographic guarantees of SHA-256 are lost to truncation, and the input itself is not a secret that needs protecting. MurmurHash3-32 achieves near-ideal avalanche properties, meaning small input changes spread evenly across the output space, which is exactly what matters for minimizing collisions in this 3-byte component. It also runs synchronously, which allowed us to remove the async initialization step that SubtleCrypto.digest() required from every consumer of the library.

Comparison with alternative solutions

Different identifier systems offer distinct advantages:

SystemStrengthsBest for
NeXIDTime-ordered (sec), URL-safe, distributedDistributed systems needing time-ordered IDs
UUID v1Time-based (100ns), uses MAC addressSystems requiring ns precision with hardware ties
UUID v4Pure randomness, standardized, widely adoptedSystems prioritizing collision resistance
UUID v7Time-ordered (ms), index locality, sortableSystems prioritizing time-based sorting
ULIDTime-ordered (ms), URL-safe (Base32), monotonicApps needing sortable IDs with ms precision
nanoidCompact, URL-safe, high performanceURL shorteners, high-volume generation
KSUIDTime-ordered (sec), URL-safe (Base62), entropySystems needing sortable IDs with sec precision
cuid2Collision-resistant, horizontal scaling, secureSecurity-focused apps needing unpredictable IDs
SnowflakeTime-ordered (ms), includes worker/DC IDsLarge-scale coordinated distributed infrastructure

UUID v4 remains ideal for pure randomness, nanoid excels when string size is critical, cuid2 prioritizes security over performance, and Snowflake IDs work well for controlled infrastructure.

Real-world applications

  • High-scale e-commerce: time-ordering with independent generation enables tracking without coordination.
  • Multi-region data synchronization: for content replication with eventual consistency, machine identifiers and timestamps simplify conflict resolution.
  • Real-time analytics: high-performance generation with chronological sorting eliminates separate sequencing.
  • Distributed file systems: lexicographical sorting optimizes indexes while machine IDs enable sharding.
  • Progressive Web Apps: client-side generation works offline while maintaining global uniqueness.
  • Time-series data management: XIDs function as both identifiers and time indices, reducing schema complexity.

CLI

NeXID ships a CLI for quick ID generation:

npx nexid          # generate a single XID

Development

npm install
npm test        # runs vitest
npm run build   # compile library
npm run bundle  # build standalone bundles (required before benchmark)
npm run benchmark

Credits

  • Original XID specification by Olivier Poitrey
  • Inspired by MongoDB's ObjectID and Twitter's Snowflake

Good reads

License

MIT License