#snowflake-id #uuid #ulid #monotonic

ferroid

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

6 releases

Uses new Rust 2024

new 0.1.5 May 17, 2025
0.1.4 May 15, 2025

#628 in Rust patterns

Download history 304/week @ 2025-05-10

304 downloads per month

MIT/Apache

86KB
1.5K SLoC

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

๐Ÿ“ฆ 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

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 mut generator = BasicSnowflakeGenerator::<SnowflakeTwitterId, _>::new(1, clock);

let id: SnowflakeTwitterId = loop {
    match generator.next_id() {
        IdGenStatus::Ready { id } => break id,
        IdGenStatus::Pending { yield_until } => {
            println!("Exhausted; wait until: {}", yield_until);
            std::hint::spin_loop();
            // Use `std::hint::spin_loop()` for single-threaded or per-thread generators.
            // Use `std::thread::yield_now()` when sharing a generator across multiple threads.
            // Use `tokio::time::sleep().await` in async contexts (e.g., Tokio thread pool).
        }
    }
};

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

Or use another pre-built layout such as Mastodon:

use ferroid::{MonotonicClock, MASTODON_EPOCH, BasicSnowflakeGenerator, SnowflakeMastodonId, IdGenStatus};

let clock = MonotonicClock::with_epoch(MASTODON_EPOCH);
let mut generator = BasicSnowflakeGenerator::<SnowflakeMastodonId, _>::new(1, clock);

// loop as above

Custom Layouts

To define a custom Snowflake layout, implement Snowflake and optionally Base32:

use ferroid::{Snowflake, Base32};

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct MyCustomId {
    id: u64,
}

// required
impl Snowflake for MyCustomId {
    // impl required methods
}

// optional, only if you need it
impl Base32 for MyCustomId {}

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() for sortable representations:

use ferroid::{SnowflakeTwitterId};

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:

  • BasicSnowflakeGenerator: single-threaded generator
  • LockSnowflakeGenerator: mutex-based, thread-safe generator
  • AtomicSnowflakeGenerator: lock-free, thread-safe generator

Benchmark scenarios include:

  • Single-threaded with/without a real clock
  • Multi-threaded with/without a real clock

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. To run:

cargo criterion

๐Ÿงช 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.

Dependencies

~0.3โ€“0.9MB
~19K SLoC