thomastheyoung/nexid
Fast, time-sortable unique identifiers for the JavaScript ecosystem. Port of the original Go library github/rs/xid.
NeXID - Fast, lexicographically sortable unique IDs
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
- For advanced usage and real-world use cases, see API reference and Use cases.
- To see NeXID in action in a web environment, visit the library's github page.
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 nexidRequires 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
});| Option | Type | Default | Description |
|---|---|---|---|
machineId |
string |
Auto-detected | Custom machine identifier string (hashed before use) |
processId |
number |
Auto-detected | Custom process ID, masked to 16-bit |
randomBytes |
(size: number) => Uint8Array |
Auto-detected | Custom CSPRNG implementation |
allowInsecure |
boolean |
false |
When false, throws if CSPRNG cannot be resolved |
filterOffensiveWords |
boolean |
false |
Reject IDs containing offensive word substrings |
offensiveWords |
string[] |
[] |
Additional words to block alongside the built-in list |
maxFilterAttempts |
number |
10 |
Max 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 fallbacksXID 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) IDInstance 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 comparisonPrefer 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.APIArchitecture
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-idon Linux,IOPlatformUUIDon macOS, registryMachineGuidon 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.pidmasked to 16-bit - Deno:
Deno.pidmasked 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 andArrayBufferfallbacks) - 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 timestampDistributed 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:
| Implementation | IDs/Second | Time sortable | Collision resistance | URL-safe | Coordination-free | Compact |
|---|---|---|---|---|---|---|
| hyperid | 53,243,635 | ✓ | ✓ | ✓ | ✓ | |
| NeXID.fastId() | 9,910,237 | ✓ | ✓ | ✓ | ✓ | ✓ |
| node randomUUID | 8,933,319 | ✓ | ✓ | |||
| uuid v4 | 8,734,995 | ✓ | ✓ | |||
| nanoid | 6,438,064 | ✓ | ✓ | ✓ | ✓ | |
| uuid v7 | 3,174,575 | ✓ | ✓ | ✓ | ||
| uuid v1 | 2,950,065 | ✓ | ✓ | ✓ | ||
| ksuid | 66,934 | ✓ | ✓ | ✓ | ✓ | ✓ |
| ulid | 48,760 | ✓ | ✓ | ✓ | ✓ | ✓ |
| cuid2 | 6,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 2^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:
| System | Strengths | Best for |
|---|---|---|
| NeXID | Time-ordered (sec), URL-safe, distributed | Distributed systems needing time-ordered IDs |
| UUID v1 | Time-based (100ns), uses MAC address | Systems requiring ns precision with hardware ties |
| UUID v4 | Pure randomness, standardized, widely adopted | Systems prioritizing collision resistance |
| UUID v7 | Time-ordered (ms), index locality, sortable | Systems prioritizing time-based sorting |
| ULID | Time-ordered (ms), URL-safe (Base32), monotonic | Apps needing sortable IDs with ms precision |
| nanoid | Compact, URL-safe, high performance | URL shorteners, high-volume generation |
| KSUID | Time-ordered (sec), URL-safe (Base62), entropy | Systems needing sortable IDs with sec precision |
| cuid2 | Collision-resistant, horizontal scaling, secure | Security-focused apps needing unpredictable IDs |
| Snowflake | Time-ordered (ms), includes worker/DC IDs | Large-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 XIDDevelopment
npm install
npm test # runs vitest
npm run build # compile library
npm run bundle # build standalone bundles (required before benchmark)
npm run benchmarkCredits
- Original XID specification by Olivier Poitrey
- Inspired by MongoDB's ObjectID and Twitter's Snowflake
Good reads
- Great and comprehensive overview of unique ID generation systems from bool.dev blog