ferroid 0.3.8

A flexible ID generator for producing unique, monotonic, and lexicographically sortable Snowflake-style IDs.
Documentation

ferroid

ferroid is a Rust crate for generating and parsing Snowflake-style unique IDs.

It supports pre-built layouts for platforms like Twitter, Discord, Instagram, and Mastodon. These IDs are 64-bit integers that encode timestamps, machine/shard IDs, and sequence numbers - making them lexicographically sortable, scalable, and ideal for distributed systems.

Features:

  • 📌 Bit-level layout compatibility with major Snowflake formats
  • 🧩 Pluggable time sources via the TimeSource trait
  • 🧵 Lock-based and lock-free thread-safe ID generation
  • 📐 Customizable layouts via the Snowflake trait
  • 🔢 Lexicographically sortable string encoding

Crates.io MIT licensed Apache 2.0 licensed CI

📦 Supported Layouts

Platform Timestamp Bits Machine ID Bits Sequence Bits Epoch
Twitter 41 10 12 2010-11-04 01:42:54.657
Discord 42 10 12 2015-01-01 00:00:00.000
Instagram 41 13 10 2011-01-01 00:00:00.000
Mastodon 48 0 16 1970-01-01 00:00:00.000

🔧 Generator Comparison

Generator Thread-Safe Lock-Free Throughput Use Case
BasicSnowflakeGenerator Highest Single-threaded, zero contention; ideal for sharded/core-local generators
LockSnowflakeGenerator Medium Multi-threaded workloads where fair access across threads is important
AtomicSnowflakeGenerator High Multi-threaded workloads where fair access is sacrificed for higher throughput

All generators produce monotonically increasing, time-ordered, and unique IDs.

🚀 Usage

Generate an ID

Synchronous

Calling next_id() may yield Pending if the current sequence is exhausted. In that case, you can spin, yield, or sleep depending on your environment:

use ferroid::{MonotonicClock, TWITTER_EPOCH, BasicSnowflakeGenerator, SnowflakeTwitterId, IdGenStatus};

let clock = MonotonicClock::with_epoch(TWITTER_EPOCH);
let generator = BasicSnowflakeGenerator::new(0, clock);

let id: SnowflakeTwitterId = loop {
    match generator.next_id() {
        IdGenStatus::Ready { id } => break id,
        IdGenStatus::Pending { yield_for } => {
            println!("Exhausted; wait for: {}ms", yield_for);
            core::hint::spin_loop();
            // Use `core::hint::spin_loop()` for single-threaded or per-thread generators.
            // Use `std::thread::yield_now()` when sharing a generator across multiple threads.
            // Use `std::thread::sleep(Duration::from_millis(yield_for.to_u64().unwrap())` to sleep.
        }
    }
};

println!("Generated ID: {}", id);

Asynchronous

If you're in an async context (e.g., using Tokio or Smol), you can enable one of the following features:

  • async-tokio - for integration with the Tokio runtime
  • async-smol - for integration with the Smol runtime

Then, import the corresponding SnowflakeGeneratorAsyncTokioExt or SnowflakeGeneratorAsyncSmolExt trait to asynchronously request a new ID without blocking or spinning.

Tokio Example

use ferroid::{
    AtomicSnowflakeGenerator, MASTODON_EPOCH, MonotonicClock, Result, SnowflakeGeneratorAsyncTokioExt,
    SnowflakeMastodonId, TokioSleep,
};

#[tokio::main]
async fn main() -> Result<()> {
    let clock = MonotonicClock::with_epoch(MASTODON_EPOCH);
    let generator = AtomicSnowflakeGenerator::new(0, clock);

    let id: SnowflakeMastodonId = generator.try_next_id_async().await?;
    println!("Generated ID: {}", id);

    Ok(())
}

Smol Example

use ferroid::{
    AtomicSnowflakeGenerator, MASTODON_EPOCH, MonotonicClock, Result, SmolSleep,
    SnowflakeGeneratorAsyncSmolExt, SnowflakeMastodonId,
};

fn main() -> Result<()> {
    smol::block_on(async {
        let clock = MonotonicClock::with_epoch(MASTODON_EPOCH);
        let generator = AtomicSnowflakeGenerator::new(0, clock);

        let id: SnowflakeMastodonId = generator.try_next_id_async().await?;
        println!("Generated ID: {}", id);

        Ok(())
    })
}

Custom Layouts

To define a custom Snowflake layout, use the define_snowflake_id macro:

use ferroid::define_snowflake_id;

// Example: a 64-bit Twitter-like ID layout
//
//  Bit Index:  63           63 62            22 21             12 11             0
//              +--------------+----------------+-----------------+---------------+
//  Field:      | reserved (1) | timestamp (41) | machine ID (10) | sequence (12) |
//              +--------------+----------------+-----------------+---------------+
//              |<----------- MSB ---------- 64 bits ----------- LSB ------------>|
define_snowflake_id!(
    MyCustomId, u64,
    reserved: 1,
    timestamp: 41,
    machine_id: 10,
    sequence: 12
);


// Example: a 128-bit extended ID layout
//
//  Bit Index:  127                88 87            40 39             20 19             0
//              +--------------------+----------------+-----------------+---------------+
//  Field:      | reserved (40 bits) | timestamp (48) | machine ID (20) | sequence (20) |
//              +--------------------+----------------+-----------------+---------------+
//              |<------- HI 64 bits ------->|<--------------- LO 64 bits ------------->|
//              |<----- MSB ------ LSB ----->|<----- MSB ------ 64 bits ----- LSB ----->|
define_snowflake_id!(
    MyCustomLongId, u128,
    reserved: 40,
    timestamp: 48,
    machine_id: 20,
    sequence: 20
);

Note: All four sections (reserved, timestamp, machine_id, and sequence) must be specified in the macro, even if a section uses 0 bits. reserved bits are always stored as zero and can be used for future expansion.

Behavior

  • If the clock advances: reset sequence to 0 → IdGenStatus::Ready
  • If the clock is unchanged: increment sequence → IdGenStatus::Ready
  • If the clock goes backward: return IdGenStatus::Pending
  • If the sequence overflows: return IdGenStatus::Pending

Serialize as padded string

Use .to_padded_string() or .encode() (enabled with base32 feature) for sortable representations:

use ferroid::{SnowflakeTwitterId, SnowflakeBase32Ext};

let id = SnowflakeTwitterId::from(123456, 1, 42);
println!("default: {id}");
// > default: 517811998762

println!("padded: {}", id.to_padded_string());
// > padded: 00000000517811998762

let encoded = id.encode();
println!("base32: {encoded}");
// > base32: 00000Y4G0082M

let decoded = SnowflakeTwitterId::decode(&encoded).expect("decode should succeed");
assert_eq!(id, decoded);

📈 Benchmarks

ferroid ships with Criterion benchmarks to measure ID generation performance.

Here's a snapshot of peak single-core throughput on a MacBook Pro 14" M1 (8 performance + 2 efficiency cores), measured under ideal conditions where the generator never yields. These numbers reflect the upper bounds of real-clock performance:

mono/sequential/basic/elems/4096
    time:   [11.747 µs 11.809 µs 11.885 µs]
    thrpt:  [344.63 Melem/s 346.85 Melem/s 348.69 Melem/s]

mono/sequential/lock/elems/4096
    time:   [38.026 µs 38.076 µs 38.134 µs]
    thrpt:  [107.41 Melem/s 107.58 Melem/s 107.72 Melem/s]

mono/sequential/atomic/elems/4096
    time:   [13.016 µs 13.055 µs 13.104 µs]
    thrpt:  [312.59 Melem/s 313.76 Melem/s 314.68 Melem/s]

And here's the equivalent theoretical maximum throughput in an async context using Tokio and Smol runtimes:

mono/sequential/async/tokio/lock/elems/4096
    time:   [38.993 µs 39.033 µs 39.075 µs]
    thrpt:  [104.82 Melem/s 104.94 Melem/s 105.04 Melem/s]
mono/sequential/async/tokio/atomic/elems/4096
    time:   [22.046 µs 22.097 µs 22.171 µs]
    thrpt:  [184.74 Melem/s 185.36 Melem/s 185.80 Melem/s]

mono/sequential/async/smol/lock/elems/4096
    time:   [38.958 µs 39.085 µs 39.241 µs]
    thrpt:  [104.38 Melem/s 104.80 Melem/s 105.14 Melem/s]
mono/sequential/async/smol/atomic/elems/4096
    time:   [21.719 µs 21.864 µs 22.136 µs]
    thrpt:  [185.04 Melem/s 187.34 Melem/s 188.59 Melem/s]

To run all benchmarks:

cargo criterion --all-features

NOTE: Shared generators (like LockSnowflakeGenerator and AtomicSnowflakeGenerator) can slow down under high thread contention. This happens because threads must coordinate access - either through mutex locks or atomic compare-and-swap (CAS) loops - which introduces overhead.

For maximum throughput, avoid sharing. Instead, give each thread its own generator instance. This eliminates contention and allows every thread to issue IDs independently at full speed.

The thread-safe generators are primarily for convenience, or for use cases where ID generation is not expected to be the performance bottleneck.

🧪 Testing

Run all tests with:

cargo test --all-features

📄 License

Licensed under either of:

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.