nulid 0.1.0

Nanosecond-Precision Universally Lexicographically Sortable Identifier
Documentation

NULID

Nanosecond-Precision Universally Lexicographically Sortable Identifier

Crates.io Documentation License Rust Version


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:

[dependencies]
nulid = "0.1"

As a CLI Tool

Install the NULID command-line tool:

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:

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::Nulid;

// Generate a new NULID
let id = Nulid::new()?;
println!("{}", id); // 01GZTV7EQ056J0E6N276XD6F3DNGMY
# Ok::<(), nulid::Error>(())

CLI Usage

# 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

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

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

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:

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:

  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

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.

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

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
  • 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

# Decode to hex
$ nulid decode 01GZTYKA3WZKB8VGCA3WHV101KRWB7
00187f5e9a87cfcd68dc18a1f23b08033c7167

# Parse a NULID
$ nulid parse 01GZTYKA3WZKB8VGCA3WHV101KRWB7
01GZTYKA3WZKB8VGCA3WHV101KRWB7

# Validate from stdin
$ cat nulids.txt | nulid validate
01GZTYKA3WZKB8VGCA3WHV101KRWB7: valid
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

cargo build --release

Testing

cargo test

Benchmarks

cargo bench

๐Ÿ“š 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 functionality
  • serde - Enable serialization/deserialization support
  • std - Standard library support (enabled by default)

To use with serde:

[dependencies]
nulid = { version = "0.1", features = ["serde"] }

๐Ÿ“œ License

Licensed under the MIT License. See LICENSE for details.


Built with โšก by developers who need nanosecond precision