❄️ SnowID Rust
A Rust implementation of a Snowflake-like ID generator with 42-bit timestamp.
Generate 64-bit unique identifiers that are:
- ⚡️ Fast (~22ns per ID with default logical generation, ~722-841ns for 1,024-ID batches)
- 📈 Time-sorted
- 🔄 Monotonic
- 🔒 Thread-safe
- 🌐 Distributed-ready
- 🎯 Minimal dependencies (
base62for encoding)
🧮 ID Structure
Example ID: 151819733950271234
Default configuration:
|------------------------------------------|------------|------------|
| TIMESTAMP (42 bits) | NODE (10) | SEQ (12) |
|------------------------------------------|------------|------------|
- Timestamp: 42 bits = 139 years from 2024-01-01 (1704067200000)
- Node ID: 10 bits = 1,024 nodes (valid range: 6-16 bits)
- Sequence: 12 bits = 4,096 IDs/ms/node
🎯 Quick Start
[]
= "3.0.0"
use SnowID;
generate() uses logical timestamp generation by default: it always returns an ID and advances the timestamp component
instead of waiting when the current millisecond's sequence range is exhausted.
🔠 Base62 Encoded IDs
Generate base62 encoded IDs (using characters 0-9, a-z, A-Z) for more compact and URL-friendly identifiers:
use SnowID;
Benefits of Base62 IDs
- 🔤 More compact representation (11 chars max vs 20 digits for u64)
- 🔗 URL-friendly (no special characters)
- 👁️ Human-readable and easier to share
- 🔄 Directly encodes the same 64-bit SnowID value
🔧 Configuration
use ;
Epochs are validated when creating a generator. SnowID rejects epochs in the future and epochs old enough that the 42-bit timestamp field cannot represent the current time, preventing silent timestamp wrap or duplicate ID collisions.
ℹ️ Available Methods
use ;
⏳ Logical Overflow Handling
Default generate() does not wait on per-millisecond sequence exhaustion; it advances a logical timestamp and returns
immediately. IDs stay unique and monotonic for the generator, but the timestamp component can run ahead of wall-clock
time during sustained overload.
For callers that must never advance logical time, use try_generate(). It returns immediately with an error when the
current millisecond has exhausted its sequence values or when the generator's logical timestamp is ahead of wall-clock.
For burst-heavy workloads, use
try_generate_batch(&mut [u64]); it reserves a contiguous sequence range with one atomic state update and returns the
number of IDs written without waiting for the next millisecond.
📊 Performance & Comparisons
Social Media Platform Configurations
| Platform | Timestamp | Node Bits | Sequence Bits | Max Nodes | IDs/ms/node before logical rollover | Default generate() Time/ID |
|---|---|---|---|---|---|---|
| 41 | 10 | 12 | 1,024 | 4,096 | ~22ns | |
| 41 | 13 | 10 | 8,192 | 1,024 | ~22ns | |
| Discord | 42 | 10 | 12 | 1,024 | 4,096 | ~22ns |
Node vs Sequence Bits Trade-off
| Node Bits | Max Nodes | IDs/ms/node before logical rollover | Default generate() Time/ID |
|---|---|---|---|
| 6 | 64 | 65,536 | ~22ns |
| 8 | 256 | 16,384 | ~22ns |
| 10 | 1,024 | 4,096 | ~22ns |
| 12 | 4,096 | 1,024 | ~22ns |
| 14 | 16,384 | 256 | ~22ns |
| 16 | 65,536 | 64 | ~24ns |
Choose configuration based on your needs:
- More nodes → Increase node bits (max 16 bits = 65,536 nodes)
- More IDs per node → Increase sequence bits (min 6 node bits = 64 nodes)
- Total bits (node + sequence) is fixed at 22 bits
For default logical generation, node_bits controls how many IDs fit in a real millisecond before the generator rolls
forward to a logical timestamp. It no longer forces the hot path to wait. On a local Apple Silicon benchmark run
(cargo bench --bench perf_hotspots -- "Hotspot Generate Capacity/node_bits/<N>"), node_bits=6, node_bits=10, and
node_bits=16 all measured around ~22ns per ID. Use try_generate() when callers prefer an immediate non-blocking
failure over logical timestamp advancement.
Shared-generator contention is the other major throughput factor. A single shared generator is lock-free, but all threads still update one atomic state. In the same local benchmark run, 8 threads sharing one generator for 1,024 IDs each took about 2.64ms, while 8 per-thread generators took about 145µs. If your topology allows it, prefer one generator per thread, worker, or shard with distinct node IDs for peak throughput.
For burst generation, generate_batch(&mut [u64]) reserves a logical timestamp range with one atomic update and fills
the whole buffer. try_generate_batch(&mut [u64]) is available when callers prefer partial non-blocking reservation.
On the same machine, filling 1,024 IDs with generate_batch() took about 721.62-840.74ns, while calling generate()
1,024 times took about 24.25µs. That is roughly a 32x throughput improvement for callers that can consume IDs in batches.
Focused hotspot benchmarks are available for validating your target machine without running the full benchmark suite:
Int64 vs Base62 Performance
| Variant | Time/ID | Size | Notes |
|---|---|---|---|
| Int64 | ~22 ns | 18-20 digits | Fastest single-ID option |
| Base62 (String) | ~40 ns | 10-11 chars | Compact, URL-friendly |
| Base62 (array) | ~24 ns | 10-11 chars | Zero-allocation, hot paths |
| Base62 (into) | ~24 ns | 10-11 chars | Zero-allocation, reuse buffer |
These Base62 numbers include ID generation plus encoding from the targeted ID Generation Comparison benchmark.
Base62 encoding provides more compact, URL-friendly IDs. For hot paths, use generate_base62_array() or generate_base62_into() to avoid heap allocations.
🚀 Examples
Check out examples for:
- Basic usage
- Custom configuration
- Base62 encoding and decoding
- Performance comparisons between Int64 and Base62
- Distributed generation
- Performance benchmarks
🧪 Correctness Stress Testing
The test suite includes deterministic unit tests, high-load multithread tests, and property-based tests for ID
uniqueness, monotonicity, sequence bounds, logical timestamp rollover, and batch generation. For Stryker-style mutation
testing in Rust, see MUTATION_TESTING.md for cargo-mutants commands and release-gate guidance.
📜 License
MIT - See LICENSE for details