# NULID
**Nanosecond-Precision Universally Lexicographically Sortable Identifier**
[](https://crates.io/crates/nulid)
[](https://docs.rs/nulid)
[](https://github.com/kakilangit/nulid/blob/main/LICENSE)
[](https://www.rust-lang.org)
---
## Overview
NULID is an extension of [ULID](https://github.com/ulid/spec) that provides **nanosecond-precision timestamps** for high-throughput, distributed systems. While the original ULID is optimal for many use-cases, some high-concurrency systems require finer granularity for true chronological sorting and enhanced collision resistance.
### Why NULID?
**The Challenge:**
- ULID's 48-bit millisecond timestamp is insufficient for high-throughput, distributed systems that generate thousands of IDs within the same millisecond
- In systems processing millions of operations per second, millisecond precision can lead to sorting ambiguities
**The Solution:**
- NULID uses a **70-bit nanosecond timestamp** for precise chronological ordering
- Preserves ULID's robust **80-bit randomness** for collision resistance
- Maintains all the benefits of ULID while extending precision
### Features
โจ **150-bit identifier** (18.75 bytes) for maximum feature set\
โก **1.21e+24 unique NULIDs per nanosecond** (80 bits of randomness)\
๐ **Lexicographically sortable** with nanosecond precision\
๐ค **30-character canonical encoding** using Crockford's Base32\
๐ **Extended lifespan** โ valid until **~45,526 AD** (4ร longer than ULID)\
๐ **Case insensitive** for flexible string handling\
๐ **URL safe** โ no special characters\
โ๏ธ **Monotonic sort order** within the same nanosecond
---
## Installation
### As a Library
Add this to your `Cargo.toml`:
```toml
[dependencies]
nulid = "0.1"
```
### As a CLI Tool
Install the NULID command-line tool:
```bash
cargo install nulid
```
This installs the `nulid` binary for generating and inspecting NULIDs from the command line.
## Performance
Benchmark results measured on modern hardware with `cargo bench`:
| **Generation** | ~1.1 ยตs | ~900,000 NULIDs/sec |
| **String Encoding** | ~71 ns | - |
| **String Decoding** | ~97 ns | - |
| **String Round-trip** | ~168 ns | - |
| **Byte Serialization** | ~0.9 ns | - |
| **Byte Deserialization** | ~1.5 ns | - |
| **Byte Round-trip** | ~2.1 ns | - |
| **Equality Check** | ~1.3 ns | - |
| **Ordering Check** | ~1.0 ns | - |
| **Sort 1,000 NULIDs** | ~2.3 ยตs | 436 Melem/s |
| **Batch (1,000)** | ~1.1 ms | 900K NULIDs/sec |
| **Concurrent (10 threads, 10K)** | ~3.3 ms | - |
| **Serde JSON Serialize** | ~104 ns | - |
| **Serde JSON Deserialize** | ~132 ns | - |
| **Serde JSON Round-trip** | ~237 ns | - |
Key performance characteristics:
- โก **Sub-microsecond generation** - ~900K IDs per second
- ๐ **Sub-nanosecond byte operations** - extremely fast binary serialization
- ๐ฆ **~71 ns string encoding** - efficient Base32 encoding
- ๐ **~237 ns JSON round-trip** - fast serde integration
- ๐ **Thread-safe** - concurrent generation across multiple threads
- ๐พ **Zero-allocation hot paths** - minimal memory overhead
## Quick Start
### Library Usage
```rust
use nulid::Nulid;
// Generate a new NULID
let id = Nulid::new()?;
println!("{}", id); // 01GZTV7EQ056J0E6N276XD6F3DNGMY
# Ok::<(), nulid::Error>(())
```
### CLI Usage
```bash
# Generate a single NULID
$ nulid generate
01GZTYKA3WZKB8VGCA3WHV101KRWB7
# Generate multiple NULIDs
$ nulid gen 5
01GZTYKA3WZKB8VGCA3WHV101KRWB7
01GZTYKA3X04XRYAYMXQKVX9BTHN9W
01GZTYKA3X09T0QVJF6V1G450QYTMR
01GZTYKA3X0BRG16DN1BY7XJEYPGJH
01GZTYKA3X0EP8JVJCDA4KVXRX4YX6
# Inspect NULID details
$ nulid inspect 01GZTYKA3WZKB8VGCA3WHV101KRWB7
NULID: 01GZTYKA3WZKB8VGCA3WHV101KRWB7
Timestamp: 1765233596749041000 (1765233596749041000 ns since epoch)
Randomness: dc18a1f23b08033c7167
Bytes: 00187f5e9a87cfcd68dc18a1f23b08033c7167
Date/Time: 2025-12-08T22:39:56.749041000 TAI
# Validate NULIDs
$ nulid validate 01GZTYKA3WZKB8VGCA3WHV101KRWB7 INVALID
01GZTYKA3WZKB8VGCA3WHV101KRWB7: valid
INVALID: invalid (Invalid length: expected 30 characters, found 7)
Valid: 1
Invalid: 1
```
## Usage Examples
### Basic Generation
```rust
use nulid::Nulid;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate a new NULID
let id = Nulid::new()?;
println!("Generated NULID: {}", id);
// Convert to string (30 characters)
let id_string = id.to_string();
println!("String: {}", id_string); // 01GZTV7EQ056J0E6N276XD6F3DNGMY
// Parse from string (case-insensitive)
let parsed: Nulid = id_string.parse()?;
assert_eq!(id, parsed);
Ok(())
}
```
### Byte Serialization
```rust
use nulid::Nulid;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let id = Nulid::new()?;
// Convert to bytes (19 bytes)
let bytes = id.to_bytes();
println!("Bytes: {:02X?}", bytes);
// Reconstruct from bytes
let restored = Nulid::from_bytes(&bytes)?;
assert_eq!(id, restored);
Ok(())
}
```
### Lexicographic Sorting
```rust
use nulid::Nulid;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ids = vec![];
for _ in 0..5 {
ids.push(Nulid::new()?);
}
// NULIDs are naturally sortable by timestamp
ids.sort();
// Verify chronological order
assert!(ids.windows(2).all(|w| w[0] < w[1]));
Ok(())
}
```
---
## ๐ ๏ธ Specification
The NULID is a **150-bit** (18.75 byte) binary identifier composed of:
```text
70_bit_time_high_precision 80_bit_randomness
70 bits 80 bits
```
### Components
#### **Timestamp** (70 bits)
- **Size:** 70-bit integer
- **Representation:** UNIX time in **nanoseconds** (ns)
- **Rationale:** 70 bits provides 4ร the lifespan of the original 68-bit design โ valid until the year **45,526 AD**
- **Encoding:** Most Significant Bits (MSB) first to ensure lexicographical sortability based on time
#### **Randomness** (80 bits)
- **Size:** 80 bits
- **Source:** Cryptographically secure source of randomness (when possible)
- **Rationale:** Preserves ULID's collision resistance with **1.21e+24** unique values per nanosecond
- **Collision Probability:** Astronomically low, even at extreme throughput
---
## ๐ Canonical String Representation
```text
tttttttttttttt rrrrrrrrrrrrrrrr
```
where:
- **`t`** = Timestamp (14 characters)
- **`r`** = Randomness (16 characters)
**Total Length:** 30 characters
### Encoding
NULID uses **Crockford's Base32** encoding, preserving the original ULID alphabet:
```text
0123456789ABCDEFGHJKMNPQRSTVWXYZ
```
**Character Exclusions:** The letters `I`, `L`, `O`, and `U` are excluded to avoid confusion and abuse.
#### Encoding Breakdown
| Timestamp | 70 | 14 | โ70 รท 5โ |
| Randomness | 80 | 16 | โ80 รท 5โ |
| **Total** | **150** | **30** | โ150 รท 5โ |
---
## ๐ข Sorting
NULIDs are **lexicographically sortable**:
- The **left-most character** is sorted first
- The **right-most character** is sorted last
- Nanosecond precision ensures IDs are sorted correctly even when multiple IDs are generated within the same millisecond
### Example Sort Order
```text
7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAV โ Earlier
7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAW
7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAX
7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAY โ Later
```
---
## โ๏ธ Monotonicity
When generating multiple NULIDs within the same nanosecond:
1. The **80-bit random component** is treated as a **monotonic counter**
2. It increments by **1 bit** in the least significant bit position (with carrying)
3. This ensures deterministic sort order within the same nanosecond
### Example
```rust
use nulid::Nulid;
// Assume these calls occur within the same nanosecond
Nulid::new(); // 7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAV
Nulid::new(); // 7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAW
Nulid::new(); // 7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAX
```
### Overflow Condition
If more than **2^80** NULIDs are generated within the same nanosecond (an extremely unlikely scenario), the generation will fail with an overflow error.
```rust,no_run
use nulid::Nulid;
// After 2^80 generations in the same nanosecond:
Nulid::new(); // panics with: "NULID overflow!"
```
---
## ๐๏ธ Binary Layout and Byte Order
The NULID components are encoded as **19 bytes (150 bits used, with 2 bits reserved)** with the **Most Significant Byte (MSB) first** (network byte order).
### Structure
```text
Byte: 0 1 2 3 4 5 6 7 8 9 ... 18
+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
^^
Reserved (2 bits) - must be 0
```
**Detailed Layout:**
- **Byte 0 (bits 0-1):** 2 reserved bits (must be 0)
- **Bytes 0-8:** 70-bit timestamp (using remaining 70 bits)
- **Bytes 9-18:** 80-bit randomness
**Reserved Bits:**
- The 2 most significant bits in byte 0 are reserved for future use
- Current specification requires these bits to be set to `00`
- Future versions may define meaning for these bits
- Decoders SHOULD accept any value but MUST preserve them
This structure ensures:
- โ
Maximum chronological sortability (nanosecond precision)
- โ
Maximum lifespan (valid until 45,526 AD)
- โ
Maximum collision resistance (80 bits of randomness)
- โ
Future extensibility (2 reserved bits)
---
## ๐ Comparison: ULID vs NULID
| **Total Bits** | 128 | 150 |
| **String Length** | 26 chars | 30 chars |
| **Timestamp Bits** | 48 (milliseconds) | 70 (nanoseconds) |
| **Randomness Bits** | 80 | 80 |
| **Time Precision** | 1 millisecond | 1 nanosecond |
| **Lifespan** | Until 10889 AD | Until 45,526 AD |
| **IDs per Time Unit** | 1.21e+24 / ms | 1.21e+24 / ns |
| **Sortable** | โ
| โ
|
| **Monotonic** | โ
| โ
|
| **URL Safe** | โ
| โ
|
---
## ๐ Features
- **Zero dependencies** for core functionality
- **Optional serde support** for serialization
- **Thread-safe** monotonic generation
- **No unsafe code**
- **Comprehensive test coverage**
- **Benchmark suite** included
## ๐ฏ Use Cases
NULID is ideal for:
- **High-frequency trading systems** requiring nanosecond-level event ordering
- **Distributed databases** with high write throughput
- **Event sourcing systems** where precise ordering is critical
- **Microservices architectures** generating many concurrent IDs
- **`IoT` platforms** processing millions of sensor readings per second
- **Real-time analytics** systems requiring precise event sequencing
---
## CLI Reference
The NULID CLI provides command-line utilities for working with NULIDs:
### Commands
- `generate, gen, g [COUNT]` - Generate NULID(s) (default: 1)
- `parse, p <NULID>` - Parse and validate a NULID string
- `inspect, i <NULID>` - Inspect NULID components in detail
- `decode, d <NULID>` - Decode NULID to hex bytes
- `validate, v [NULID...]` - Validate NULID(s) from args or stdin
- `help, -h, --help` - Print help message
- `version, -v, --version` - Print version information
### Examples
```bash
# Decode to hex
$ nulid decode 01GZTYKA3WZKB8VGCA3WHV101KRWB7
00187f5e9a87cfcd68dc18a1f23b08033c7167
# Parse a NULID
$ nulid parse 01GZTYKA3WZKB8VGCA3WHV101KRWB7
01GZTYKA3WZKB8VGCA3WHV101KRWB7
# Validate from stdin
01GZTYKA3X04XRYAYMXQKVX9BTHN9W: valid
Valid: 2
Invalid: 0
# Use in shell scripts
$ for i in {1..3}; do nulid gen; done
01GZTYKA3WZKB8VGCA3WHV101KRWB7
01GZTYKA3X04XRYAYMXQKVX9BTHN9W
01GZTYKA3X09T0QVJF6V1G450QYTMR
```
---
## ๐ Security Considerations
1. **Use cryptographically secure random number generators** when possible
2. **Do not rely on NULID for security purposes** โ use proper authentication/authorization
3. **Timestamp information is exposed** โ NULIDs reveal when they were created
4. **Randomness must be unpredictable** โ avoid weak PRNG implementations
---
## ๐ ๏ธ Development
### Building
```bash
cargo build --release
```
### Testing
```bash
cargo test
```
### Benchmarks
```bash
cargo bench
```
## ๐ Background
NULID builds upon the excellent work of the [ULID specification](https://github.com/ulid/spec). The original ULID addressed many shortcomings of UUID:
- โ UUID v1/v2 requires access to MAC addresses
- โ UUID v3/v5 requires unique seeds and produces random distribution
- โ UUID v4 provides no temporal information
- โ UUID uses inefficient encoding (36 characters for 128 bits)
NULID extends this foundation by addressing the millisecond precision limitation while maintaining ULID's core benefits.
---
## ๐ฆ Cargo Features
- `default` - Core NULID functionality
- `serde` - Enable serialization/deserialization support
- `std` - Standard library support (enabled by default)
To use with serde:
```toml
[dependencies]
nulid = { version = "0.1", features = ["serde"] }
```
---
## ๐ License
Licensed under the MIT License. See [LICENSE](https://github.com/kakilangit/nulid/blob/main/LICENSE) for details.
---
**Built with โก by developers who need nanosecond precision**