jet-id is a tiny, zero-dependency unique ID generator for JavaScript and TypeScript. IDs are 28-character Crockford base32 strings, with optional timestamps so they sort by creation time.
import jetId from 'jet-id';
jetId(); // '9Q8SWWBTY-7NVXM-FT9S6-XB4R3M'
jetId.timed(); // '1KKNTQ2CN-606ZG-8VF48-76B6F3' (sorts by creation time)
seanpmaxwell
/
jet-id
An extremely fast unique ID generator for JavaScript and TypeScript, with optional sorting/timestamping.
jet-id
An extremely fast unique ID generator for JavaScript and TypeScript, with optional sorting/timestamping.
Quick Glance
import jetId from 'jet-id';
jetId(); // '9Q8SWWBTY-7NVXM-FT9S6-XB4R3M'
Every ID is 25 random characters from the Crockford base32 alphabet, split into groups of 9-5-5-6.
Why jet-id?
-
Main features
- Optionally timestamped and readable: Generated IDs are 28-character strings (25 Crockford characters plus 3 dashes). The first segment is 9 characters; if you want a timestamp, the first segment encodes the current epoch in Crockford form, so the ID length stays the same.
-
Fast: Faster than
nanoid(). See benchmarks.
-
Other perks
- Small: Only 6.4 kB packed.
- Strong randomness: 125 random bits. For perspective, UUID v4 has 122.
- Zero runtime dependencies.
-
Simple API: Just
jetId()andjetId.test/jetId.timed/jetId.parseTimed. - Universal: Works in Node.js and modern browsers.
Install
npm install jet-id
API
Basic usage
Just call the default import, there…
- ~83 million IDs/sec, vs. nanoid's ~52 million on my machine
-
125 random bits from
crypto.getRandomValues(UUID v4 has 122) -
Readable: the Crockford alphabet has no
I,L,O, orUto confuse with1and0 - Small: zero dependencies, 6.4 kB packed
- Universal: runs in Node.js and modern browsers, and there's a CLI too
How it started
I was looking for an ID generator for a web project (mostly for a database column) that used the Crockford alphabet and had optional timestamping. I couldn't find anything that quite fit, so I debated between wrapping nanoid and writing my own. It seemed like a fun project that probably just involved some simple arithmetic (or so I naively thought), so I wrote my own.
The first version took a morning. Then, out of curiosity, I fed it to Fable 5.1 and GPT Astra, and I was blown away. They introduced me to techniques I didn't know existed, like pooling, lookup tables, and bit-shifting. I spent the next two days passing it back and forth between them, benchmarking every change, rolling back anything that got slower, and adding my own tweaks in between.
What makes it fast
Most of the speed comes from doing expensive work rarely and in bulk:
-
Pooling. IDs are built 256 at a time into a single string, so most calls to
jetId()are just asubstring. Onecrypto.getRandomValuescall supplies enough randomness for 1,024 IDs. - Two characters per lookup. A 1,024-entry table turns 10 random bits into two Crockford characters at once, instead of encoding one character at a time.
-
Four bytes at a time. Each 28-character ID is written as seven 32-bit words through a
Uint32Array, dashes included. -
One string conversion per chunk. The bytes are decoded to a string once (through
Bufferin Node,TextDecoderin browsers), and every ID is sliced from it.
Benchmarks
On my MacBook M4 Pro (Node 24), median of 7 samples of at least 500 ms each, after a 500 ms warmup:
| Generator | Chars | Random bits | Ops/sec | Relative |
|---|---|---|---|---|
jetId() |
28 | 125 | 83,503,288 | 1.00x |
nanoid() |
21 | 126 | 52,415,897 | 0.63x |
| nanoid, Crockford, 25 chars | 25 | 125 | 47,073,761 | 0.56x |
| nanoid, Crockford, 9-5-5-6 | 28 | 125 | 14,441,127 | 0.17x |
crypto.randomUUID() |
36 | 122 | 9,938,321 | 0.12x |
uuid v4 |
36 | 122 | 8,536,998 | 0.10x |
That's roughly 60% faster than nanoid's defaults, even though jet-id's IDs are longer (28 characters vs. 21). Configured to produce the same 9-5-5-6 format, nanoid is about 6x slower. To be fair, nanoid isn't built for dash-separated output, so part of that gap is the formatting wrapper.
Why 9-5-5-6?
It looks a bit different, but it isn't arbitrary. The first segment is 9 characters because that's how long the current epoch (in milliseconds) is in Crockford base32, which leaves room for the optional timestamp without changing the ID's length. I wanted at least UUID v4's entropy, which takes 25 random characters, and splitting the remaining 16 into 5-5-6 seemed the most readable.
Timestamped IDs
jetId.timed() encodes the current time in the first nine characters, so plain string sorting is creation order. You can also pass your own epoch in milliseconds, and jetId.parseTimed() reads it back out:
jetId.timed(Date.UTC(2015, 2, 14)); // '19GAS9400-...'
const id = jetId.timed(); // '1KKNTQ2CN-606ZG-8VF48-76B6F3'
jetId.parseTimed(id); // 1773480413589
new Date(jetId.parseTimed(id)); // 2026-03-14T09:26:53.589Z
Two tradeoffs to know about: timestamped IDs spend those first 9 characters on the time, so they carry 80 random bits instead of 125, and IDs created in the same millisecond have no defined order between them.
Validating IDs
jetId.test() checks whether a value is a well-formed ID. It isn't case-sensitive:
jetId.test('avz6yg1rb-47j6r-xgns9-tqw29a'); // true
From the command line
npx jet-id # one random ID
npx jet-id -c 5 # five, one per line
npx jet-id -t # one timestamped ID
Feedback welcome
These benchmarks were all done on my local machine using the playground/benchmark.ts script. If they're different for you, or if you know of something faster, feedback is very welcome, especially if there's a flaw in the way I did the benchmarking.
Top comments (2)
I'm curious how the pool behaves when callers retain IDs from several refill cycles. Does the backing string become a measurable retention cost in a long-running process?
Yes, it's a deliberate tradeoff: each ID is a V8 sliced string pointing into a shared ~7 KB chunk of 256 IDs, so retaining one keeps its chunk in memory. For comparison, nanoid uses a 32 KB chunk.