| Crates.io | nulid |
| lib.rs | nulid |
| version | 0.7.0 |
| created_at | 2025-12-08 23:01:31.217186+00 |
| updated_at | 2026-01-20 11:11:01.585956+00 |
| description | Nanosecond-Precision Universally Lexicographically Sortable Identifier |
| homepage | https://github.com/kakilangit/nulid |
| repository | https://github.com/kakilangit/nulid |
| max_upload_size | |
| id | 1974500 |
| size | 405,718 |
Nanosecond-Precision Universally Lexicographically Sortable Identifier
NULID is a 128-bit identifier with true nanosecond-precision timestamps designed for high-throughput, distributed systems. It combines the simplicity of ULID with sub-millisecond precision for systems that require fine-grained temporal ordering.
True nanosecond precision is achieved using the quanta crate, which provides high-resolution monotonic timing combined with wall-clock synchronization. This ensures proper ordering even on systems where the OS clock only provides microsecond precision.
The Challenge:
The Solution:
128-bit identifier (16 bytes) - UUID-compatible size
High-performance - 11.78ns per ID generation
Lexicographically sortable with true nanosecond precision
26-character canonical encoding using Crockford's Base32
Extended lifespan - valid until year ~11,326 AD
Memory safe - zero unsafe code, panic-free production paths
URL safe - no special characters
Monotonic sort order within the same nanosecond
UUID interoperability - seamless conversion to/from UUID
1.15 quintillion unique IDs per nanosecond (60 bits of randomness)
True nanosecond precision - powered by quanta for high-resolution timing on all platforms
Add this to your Cargo.toml:
[dependencies]
nulid = "0.7"
With optional features:
[dependencies]
nulid = { version = "0.7", features = ["uuid"] } # UUID conversion
nulid = { version = "0.7", features = ["derive"] } # Id derive macro
nulid = { version = "0.7", features = ["macros"] } # nulid!() macro
nulid = { version = "0.7", features = ["serde"] } # Serialization
nulid = { version = "0.7", features = ["sqlx"] } # PostgreSQL support
nulid = { version = "0.7", features = ["postgres-types"] } # PostgreSQL types
nulid = { version = "0.7", features = ["rkyv"] } # Zero-copy serialization
nulid = { version = "0.7", features = ["chrono"] } # DateTime<Utc> support
use nulid::Nulid;
# fn main() -> nulid::Result<()> {
// Generate a new NULID
let id = Nulid::new()?;
println!("{}", id); // "01AN4Z07BY79K47PAZ7R9SZK18"
// Parse from string
let parsed: Nulid = "01AN4Z07BY79K47PAZ7R9SZK18".parse()?;
// Extract components
let nanos = id.nanos(); // u128: nanoseconds since epoch
let micros = id.micros(); // u128: microseconds since epoch
let millis = id.millis(); // u128: milliseconds since epoch
let random = id.random(); // u64: 60-bit random value
# Ok(())
# }
nulid!() MacroWith the macros feature:
use nulid::nulid;
// Simple generation (panics on error)
let id = nulid!();
// With error handling
fn example() -> Result<(), Box<dyn std::error::Error>> {
let id = nulid!(?)?;
Ok(())
}
// Multiple IDs
let (id1, id2, id3) = (nulid!(), nulid!(), nulid!());
Id DeriveWith the derive feature:
use nulid::Nulid;
use nulid_derive::Id;
#[derive(Id, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(Nulid);
#[derive(Id, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OrderId(Nulid);
fn example() -> Result<(), Box<dyn std::error::Error>> {
// Type-safe IDs that can't be mixed
let user_id = UserId::from(nulid::Nulid::new()?);
let order_id = OrderId::from(nulid::Nulid::new()?);
// Parse from strings
let user_id: UserId = "01H0JQ4VEFSBV974PRXXWEK5ZW".parse()?;
// Display, FromStr, TryFrom, AsRef all implemented automatically
println!("{}", user_id);
Ok(())
}
use nulid::Nulid;
# fn main() -> nulid::Result<()> {
let id = Nulid::new()?;
// Convert to/from bytes
let bytes = id.to_bytes(); // [u8; 16]
let id2 = Nulid::from_bytes(bytes);
// Ergonomic conversions using standard traits
let id3: Nulid = bytes.into(); // From<[u8; 16]>
let bytes2: [u8; 16] = id.into(); // Into<[u8; 16]>
let value: u128 = id.into(); // Into<u128>
let id4: Nulid = value.into(); // From<u128>
// Safe conversion from byte slices
let slice: &[u8] = &bytes;
let id5 = Nulid::try_from(slice)?; // TryFrom<&[u8]>
# Ok(())
# }
use nulid::Generator;
# fn main() -> nulid::Result<()> {
let generator = Generator::new();
// Generate multiple IDs - guaranteed strictly increasing
let id1 = generator.generate()?;
let id2 = generator.generate()?;
let id3 = generator.generate()?;
assert!(id1 < id2);
assert!(id2 < id3);
# Ok(())
# }
For distributed systems requiring guaranteed cross-node uniqueness:
use nulid::generator::{Generator, SystemClock, CryptoRng, WithNodeId};
# fn main() -> nulid::Result<()> {
// Each node gets a unique ID (0-65535)
let generator = Generator::<SystemClock, CryptoRng, WithNodeId>::with_node_id(1);
let id = generator.generate()?;
// Node ID is embedded in the random bits
assert_eq!(generator.node_id(), Some(1));
# Ok(())
# }
The generator supports dependency injection for testing clock skew scenarios:
use nulid::generator::{Generator, MockClock, SeededRng, NoNodeId};
use std::time::Duration;
# fn main() -> nulid::Result<()> {
// Create mock clock and seeded RNG for reproducible tests
let clock = MockClock::new(1_000_000_000);
let rng = SeededRng::new(42);
let generator = Generator::<_, _, NoNodeId>::with_deps(&clock, &rng);
let id1 = generator.generate()?;
// Simulate clock regression (NTP correction)
clock.regress(Duration::from_millis(100));
let id2 = generator.generate()?;
// Still monotonic despite clock going backward!
assert!(id2 > id1);
# Ok(())
# }
SQLx PostgreSQL SupportWith the optional sqlx feature, you can store NULIDs directly in PostgreSQL as UUIDs:
use nulid::Nulid;
use sqlx::{PgPool, Row};
#[derive(sqlx::FromRow)]
struct User {
id: Nulid,
name: String,
}
async fn insert_user(pool: &PgPool, id: Nulid, name: &str) -> sqlx::Result<()> {
sqlx::query("INSERT INTO users (id, name) VALUES ($1, $2)")
.bind(id) // Automatically converts to UUID
.bind(name)
.execute(pool)
.await?;
Ok(())
}
async fn get_user(pool: &PgPool, id: Nulid) -> sqlx::Result<User> {
sqlx::query_as::<_, User>("SELECT id, name FROM users WHERE id = $1")
.bind(id)
.fetch_one(pool)
.await
}
This enables:
PostgreSQL UUID typePostgreSQL's native UUID indexesWith the optional uuid feature, you can seamlessly convert between NULID and UUID:
use nulid::Nulid;
use uuid::Uuid;
// Generate a NULID
let nulid = Nulid::new()?;
// Convert to UUID
let uuid: Uuid = nulid.into();
println!("UUID: {}", uuid); // "01234567-89ab-cdef-0123-456789abcdef"
// Convert back to NULID
let nulid2: Nulid = uuid.into();
assert_eq!(nulid, nulid2);
// Or use explicit methods
let uuid2 = nulid.to_uuid();
let nulid3 = Nulid::from_uuid(uuid2);
This enables:
MySQL, etc.DateTime SupportWith the optional chrono feature, you can convert between NULIDs and chrono::DateTime<Utc>:
use nulid::Nulid;
use chrono::{DateTime, Utc, TimeZone};
// Generate a NULID
let id = Nulid::new()?;
// Convert to DateTime<Utc>
let dt: DateTime<Utc> = id.chrono_datetime();
println!("Timestamp: {}", dt); // "2025-12-23 10:30:45.123456789 UTC"
// Create NULID from DateTime<Utc>
let dt = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let id = Nulid::from_chrono_datetime(dt)?;
// Works with derived Id types too
#[derive(Id)]
struct UserId(Nulid);
let user_id = UserId::new()?;
let created_at = user_id.chrono_datetime();
let dt = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
let user_id = UserId::from_chrono_datetime(dt)?;
This enables:
Human-readable timestamps - Convert NULID timestamps to standard DateTime format
Time-based queries - Easy integration with chrono-based time operations
Nanosecond precision - Full nanosecond precision is preserved
Bidirectional conversion - Create NULIDs from DateTime or extract DateTime from NULIDs
Timezone support - Uses DateTime in UTC for consistency
use nulid::Nulid;
# fn main() -> nulid::Result<()> {
let mut ids = vec![
Nulid::new()?,
Nulid::new()?,
Nulid::new()?,
];
// NULIDs are naturally sortable by timestamp
ids.sort();
// Verify chronological order
assert!(ids.windows(2).all(|w| w[0] < w[1]));
# Ok(())
# }
The NULID is a 128-bit (16 byte) binary identifier composed of:
68-bit timestamp (nanoseconds) 60-bit randomness
|--------------------------------| |--------------------------|
Timestamp Randomness
68 bits 60 bits
rand crate with system entropyttttttttttttt rrrrrrrrrrrrr
where:
t = Timestamp (13 characters)r = Randomness (13 characters)Total Length: 26 characters
NULID uses Crockford's Base32 encoding:
0123456789ABCDEFGHJKMNPQRSTVWXYZ
Character Exclusions: The letters I, L, O, and U are excluded to avoid confusion.
| Component | Bits | Characters | Calculation |
|---|---|---|---|
| Timestamp | 68 | 14 | ⌈68 ÷ 5⌉ |
| Randomness | 60 | 12 | ⌈60 ÷ 5⌉ |
| Total | 128 | 26 | ⌈128 ÷ 5⌉ |
Note: Due to Base32 encoding (5 bits per character), we need 26 characters for 128 bits (130 bits capacity, with 2 bits unused).
NULIDs are lexicographically sortable:
01AN4Z07BY79K47PAZ7R9SZK18 ← Earlier
01AN4Z07BY79K47PAZ7R9SZK19
01AN4Z07BY79K47PAZ7R9SZK1A
01AN4Z07BY79K47PAZ7R9SZK1B ← Later
The Generator ensures strictly monotonic IDs:
use nulid::Generator;
# fn main() -> nulid::Result<()> {
let generator = Generator::new();
// Even if called within the same nanosecond
let id1 = generator.generate()?; // ...XYZ
let id2 = generator.generate()?; // ...XYZ + 1
let id3 = generator.generate()?; // ...XYZ + 2
assert!(id1 < id2 && id2 < id3);
# Ok(())
# }
With 60 bits of randomness, you can generate 2^60 (1.15 quintillion) IDs within the same nanosecond before overflow. This is practically impossible in real-world usage.
The NULID is encoded as 16 bytes with Most Significant Byte (MSB) first (network byte order / big-endian).
Byte: 0 1 2 3 4 5 6 7
+-------+-------+-------+-------+-------+-------+-------+-------+
Bits: | Timestamp (68 bits) - nanoseconds since epoch |
+-------+-------+-------+-------+-------+-------+-------+-------+
Byte: 8 9 10 11 12 13 14 15
+-------+-------+-------+-------+-------+-------+-------+-------+
Bits: | T | Randomness (60 bits) |
+-------+-------+-------+-------+-------+-------+-------+-------+
Detailed Layout:
This structure ensures:
| Feature | ULID | NULID |
|---|---|---|
| Total Bits | 128 | 128 |
| String Length | 26 chars | 26 chars |
| Timestamp Bits | 48 (milliseconds) | 68 (nanoseconds) |
| Randomness Bits | 80 | 60 |
| Time Precision | 1 millisecond | 1 nanosecond |
| Lifespan | Until 10889 AD | Until 11,326 AD |
| IDs per Time Unit | 1.21e+24 / ms | 1.15e+18 / ns |
| Sortable | ✅ | ✅ |
| Monotonic | ✅ | ✅ |
| URL Safe | ✅ | ✅ |
| UUID Compatible | ✅ | ✅ |
rand crate for amortized cryptographic randomness#![forbid(unsafe_code)]ResultMessagePack, Bincode, etc.)
MessagePack) use efficient 16-byte encodingSQLx support for PostgreSQL UUID storageThe nulid binary provides a powerful CLI for working with NULIDs.
Install the CLI with all features enabled:
cargo install nulid --all-features
Or build from source:
cargo build --bin nulid --release --features "uuid,chrono"
# Generate NULIDs
nulid generate # Generate one NULID
nulid gen 10 # Generate 10 NULIDs
# Inspect NULID details
nulid inspect 01GZWQ22K2MNDR0GAQTE834QRV
# Output shows: timestamp, random bits, bytes, datetime, UUID (if feature enabled)
# Parse and validate
nulid parse 01GZWQ22K2MNDR0GAQTE834QRV
nulid validate 01GZWQ22K2MNDR0GAQTE834QRV 01GZWQ22K2TKVGHH1Z1G0AK1EK
# Compare two NULIDs
nulid compare 01GZWQ22K2MNDR0GAQTE834QRV 01GZWQ22K2TKVGHH1Z1G0AK1EK
# Shows which is earlier and time difference in nanoseconds
# Sort NULIDs chronologically
nulid sort 01GZWQ22K2TKVGHH1Z1G0AK1EK 01GZWQ22K2MNDR0GAQTE834QRV
cat nulids.txt | nulid sort
# Decode to hex
nulid decode 01GZWQ22K2MNDR0GAQTE834QRV
--features uuid)# Convert NULID to UUID
nulid uuid 01GZWQ22K2MNDR0GAQTE834QRV
# Convert UUID to NULID
nulid from-uuid 018d3f9c-5a2e-7b4d-8f1c-3e6a9d2c5b7e
DateTime Commands (requires --features chrono)# Convert NULID to ISO 8601 datetime
nulid datetime 01GZWQ22K2MNDR0GAQTE834QRV
# Output: 2024-01-01T00:00:00.123456789+00:00
# Create NULID from datetime
nulid from-datetime 2024-01-01T00:00:00Z
NULID is ideal for:
PostgreSQL UUID storage via sqlx)IoT platforms processing millions of sensor readings per secondPostgreSQL applications - Store as native UUID with time-based orderingpub struct Nulid(u128);
impl Nulid {
// Generation
pub fn new() -> Result<Self>;
pub fn now() -> Result<Self>;
// Construction
pub const fn from_nanos(timestamp_nanos: u128, rand: u64) -> Self;
pub const fn from_u128(value: u128) -> Self;
pub const fn from_bytes(bytes: [u8; 16]) -> Self;
pub fn from_str(s: &str) -> Result<Self>;
// Extraction
pub const fn nanos(self) -> u128; // Nanoseconds
pub const fn micros(self) -> u128; // Microseconds
pub const fn millis(self) -> u128; // Milliseconds
pub const fn random(self) -> u64;
pub const fn parts(self) -> (u128, u64);
// Conversion
pub const fn as_u128(self) -> u128;
pub const fn to_bytes(self) -> [u8; 16];
pub fn encode(self, buf: &mut [u8; 26]);
// UUID interoperability (with `uuid` feature)
#[cfg(feature = "uuid")]
pub fn to_uuid(self) -> uuid::Uuid;
#[cfg(feature = "uuid")]
pub fn from_uuid(uuid: uuid::Uuid) -> Self;
// Time utilities
pub fn datetime(self) -> SystemTime;
pub fn duration_since_epoch(self) -> Duration;
// Chrono DateTime (with `chrono` feature)
#[cfg(feature = "chrono")]
pub fn chrono_datetime(self) -> chrono::DateTime<chrono::Utc>;
#[cfg(feature = "chrono")]
pub fn from_chrono_datetime(dt: chrono::DateTime<chrono::Utc>) -> Result<Self>;
// Utilities
pub const fn nil() -> Self;
pub const fn is_nil(self) -> bool;
// Constants
pub const MIN: Self;
pub const MAX: Self;
pub const ZERO: Self;
}
// Standard trait implementations for ergonomic conversions
impl From<u128> for Nulid { }
impl From<Nulid> for u128 { }
impl From<[u8; 16]> for Nulid { }
impl From<Nulid> for [u8; 16] { }
impl AsRef<u128> for Nulid { }
impl TryFrom<&[u8]> for Nulid { }
// UUID conversions (with `uuid` feature)
#[cfg(feature = "uuid")]
impl From<uuid::Uuid> for Nulid { }
#[cfg(feature = "uuid")]
impl From<Nulid> for uuid::Uuid { }
// Standard traits
impl Display for Nulid { }
impl FromStr for Nulid { }
impl Ord for Nulid { }
impl Default for Nulid { } // Returns Nulid::ZERO
// Unified generator with injectable dependencies
pub struct Generator<C: Clock = SystemClock, R: Rng = CryptoRng, N: NodeId = NoNodeId> { }
impl Generator<SystemClock, CryptoRng, NoNodeId> {
pub const fn new() -> Self; // Production single-node
}
impl Generator<SystemClock, CryptoRng, WithNodeId> {
pub fn with_node_id(node_id: u16) -> Self; // Production distributed
}
impl<C: Clock, R: Rng, N: NodeId> Generator<C, R, N> {
pub fn with_deps(clock: C, rng: R) -> Self; // Testing
pub fn with_deps_and_node_id(clock: C, rng: R, node_id: N) -> Self;
pub fn generate(&self) -> Result<Nulid>;
pub fn last(&self) -> Option<Nulid>;
pub fn reset(&self);
pub fn node_id(&self) -> Option<u16>;
}
// Type aliases
pub type DefaultGenerator = Generator<SystemClock, CryptoRng, NoNodeId>;
pub type DistributedGenerator = Generator<SystemClock, CryptoRng, WithNodeId>;
// Clock abstraction
pub trait Clock: Send + Sync {
fn now_nanos(&self) -> Result<u128>;
}
pub struct SystemClock; // Production: uses quanta
pub struct MockClock; // Testing: controllable time
impl MockClock {
pub fn new(initial_nanos: u64) -> Self;
pub fn set(&self, nanos: u64);
pub fn advance(&self, duration: Duration);
pub fn regress(&self, duration: Duration); // Simulate clock going backward
}
// RNG abstraction
pub trait Rng: Send + Sync {
fn random_u64(&self) -> u64;
}
pub struct CryptoRng; // Production: cryptographic RNG
pub struct SeededRng; // Testing: reproducible sequences
pub struct SequentialRng; // Debugging: 0, 1, 2, 3...
// Node ID abstraction
pub trait NodeId: Send + Sync + Default + Copy {
fn get(&self) -> Option<u16>;
}
pub struct NoNodeId; // Default: 60 bits random (ZST, zero overhead)
pub struct WithNodeId(u16); // Distributed: 16 bits node + 44 bits random
pub enum Error {
RandomError,
InvalidChar(char, usize),
InvalidLength { expected: usize, found: usize },
MutexPoisoned,
}
pub type Result<T> = std::result::Result<T, Error>;
default = ["std"] - Standard library supportstd - Enable standard library features (SystemTime, etc.)derive - Enable Id derive macro for type-safe wrapper types (requires nulid_derive)macros - Enable nulid!() macro for convenient generation (requires nulid_macros)serde - Enable serialization/deserialization support (JSON, TOML, MessagePack, Bincode, etc.)uuid - Enable UUID interoperability (conversion to/from uuid::Uuid)sqlx - Enable SQLx PostgreSQL support (stores as UUID, requires uuid feature)postgres-types - Enable PostgreSQL postgres-types crate supportrkyv - Enable zero-copy serialization supportchrono - Enable chrono::DateTime<Utc> conversion supportExamples:
# With serde (supports JSON, TOML, MessagePack, Bincode, etc.)
[dependencies]
nulid = { version = "0.7", features = ["serde"] }
# With UUID interoperability
[dependencies]
nulid = { version = "0.7", features = ["uuid"] }
# With derive macro for type-safe IDs
[dependencies]
nulid = { version = "0.7", features = ["derive"] }
nulid_derive = "0.7"
# With convenient nulid!() macro
[dependencies]
nulid = { version = "0.7", features = ["macros"] }
# With both derive and macros
[dependencies]
nulid = { version = "0.7", features = ["derive", "macros"] }
nulid_derive = "0.7"
# With SQLx PostgreSQL support
[dependencies]
nulid = { version = "0.7", features = ["sqlx"] }
# With chrono DateTime support
[dependencies]
nulid = { version = "0.7", features = ["chrono"] }
# All features
[dependencies]
nulid = { version = "0.7", features = ["derive", "macros", "serde", "uuid", "sqlx", "postgres-types", "rkyv", "chrono"] }
nulid_derive = "0.7"
The serde_example demonstrates multiple formats including JSON, MessagePack, TOML, and Bincode:
# Run the serde examples (includes Bincode)
cargo run --example serde_example --features serde
For the sqlx example, see examples/sqlx_postgres.rs:
# Set up PostgreSQL database
export DATABASE_URL="postgresql://localhost/nulid_example"
createdb nulid_example
# Run the example
cargo run --example sqlx_postgres --features sqlx
rand crate with system entropy for high-quality randomnesscargo build --release
cargo test
cargo bench
| Operation | Time | Throughput |
|---|---|---|
| Generate new NULID | 11.78 ns | 84.9M ops/sec |
| From datetime | 14.11 ns | 70.9M ops/sec |
| Monotonic generation | 20.96 ns | 47.7M ops/sec |
| Sequential generation (100 IDs) | 2.10 µs | 47.5M IDs/sec |
| Encode to string (array) | 9.10 ns | 110M ops/sec |
| Encode to String (heap) | 32.84 ns | 30.5M ops/sec |
| Decode from string | 8.87 ns | 113M ops/sec |
| Round-trip string | 42.04 ns | 23.8M ops/sec |
| Convert to bytes | 293.75 ps | 3.40B ops/sec |
| Convert from bytes | 392.82 ps | 2.55B ops/sec |
| Equality comparison | 2.80 ns | 357M ops/sec |
| Ordering comparison | 2.82 ns | 355M ops/sec |
| Sort 1000 IDs | 13.02 µs | 76.8M elem/sec |
| Concurrent (10 threads) | 183.60 µs | 5.45K batch/sec |
| Batch generate 10 | 234.25 ns | 42.7M elem/sec |
| Batch generate 100 | 2.23 µs | 44.7M elem/sec |
| Batch generate 1000 | 21.53 µs | 46.4M elem/sec |
Benchmarked on Apple M2 Pro processor with cargo bench
cargo clippy -- -D warnings
NULID builds upon the excellent ULID specification and addresses:
NULID achieves:
Licensed under the MIT License. See LICENSE for details.
Built with by developers who need nanosecond precision in 128 bits