NULID
Nanosecond-Precision Universally Lexicographically Sortable Identifier
Overview
NULID is an extension of ULID 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:
[]
= "0.1"
As a CLI Tool
Install the NULID command-line tool:
This installs the nulid binary for generating and inspecting NULIDs from the command line.
Performance
Benchmark results measured on modern hardware with cargo bench:
| Operation | Time | Throughput |
|---|---|---|
| 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
use Nulid;
// Generate a new NULID
let id = new?;
println!; // 01GZTV7EQ056J0E6N276XD6F3DNGMY
# Ok::
CLI Usage
# Generate a single NULID
# Generate multiple NULIDs
# Inspect NULID details
)
# Validate NULIDs
)
Usage Examples
Basic Generation
use Nulid;
Byte Serialization
use Nulid;
Lexicographic Sorting
use Nulid;
๐ ๏ธ Specification
The NULID is a 150-bit (18.75 byte) binary identifier composed of:
70_bit_time_high_precision 80_bit_randomness
|--------------------------------| |--------------------------------|
Timestamp 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
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:
0123456789ABCDEFGHJKMNPQRSTVWXYZ
Character Exclusions: The letters I, L, O, and U are excluded to avoid confusion and abuse.
Encoding Breakdown
| Component | Bits | Characters | Calculation |
|---|---|---|---|
| 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
7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAV โ Earlier
7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAW
7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAX
7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAY โ Later
โ๏ธ Monotonicity
When generating multiple NULIDs within the same nanosecond:
- The 80-bit random component is treated as a monotonic counter
- It increments by 1 bit in the least significant bit position (with carrying)
- This ensures deterministic sort order within the same nanosecond
Example
use Nulid;
// Assume these calls occur within the same nanosecond
new; // 7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAV
new; // 7VVV09D8H01ARZ3NDEKTSV4RRFFQ69G5FAW
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.
use Nulid;
// After 2^80 generations in the same nanosecond:
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
Byte: 0 1 2 3 4 5 6 7 8 9 ... 18
+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
Bits: |RR| Timestamp (70 bits) | T|R | Randomness (80 bits) ...
+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
^^
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
| Feature | ULID | 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
IoTplatforms 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 stringinspect, i <NULID>- Inspect NULID components in detaildecode, d <NULID>- Decode NULID to hex bytesvalidate, v [NULID...]- Validate NULID(s) from args or stdinhelp, -h, --help- Print help messageversion, -v, --version- Print version information
Examples
# Decode to hex
# Parse a NULID
# Validate from stdin
|
# Use in shell scripts
; do ; done
๐ Security Considerations
- Use cryptographically secure random number generators when possible
- Do not rely on NULID for security purposes โ use proper authentication/authorization
- Timestamp information is exposed โ NULIDs reveal when they were created
- Randomness must be unpredictable โ avoid weak PRNG implementations
๐ ๏ธ Development
Building
Testing
Benchmarks
๐ Background
NULID builds upon the excellent work of the ULID specification. 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 functionalityserde- Enable serialization/deserialization supportstd- Standard library support (enabled by default)
To use with serde:
[]
= { = "0.1", = ["serde"] }
๐ License
Licensed under the MIT License. See LICENSE for details.
Built with โก by developers who need nanosecond precision