TypedFlake
Distributed, type-safe Snowflake ID generation for Rust.
Generate unique, time-ordered 64-bit IDs across distributed systems without coordination. Inspired by Twitter's Snowflake algorithm, with strong type safety through Rust's newtype pattern.
Features
- π Distributed: Multi-server support via worker/process IDs
- π Thread-safe: Lock-free atomic operations using compare-and-swap
- β±οΈ Time-ordered: IDs are sortable by creation time
- π‘οΈ Type-safe: Each ID type is distinct (no mixing
UserIdwithOrderId) - ποΈ Customizable: Configure bit allocation and epoch per-type or globally
- πΎ Shared state pool: Generators share lazy state per (worker, process) pair
- π Battle-tested: Industry-standard presets from Twitter and Discord
- π¦ Serde support: Optional JSON serialization as strings (IEEE 754 safe)
Quick Start
// Define ID types
id!;
id!;
Basic Generation
Generate IDs using the default instance (worker=0, process=0):
id!;
let id = generate;
Multiple ID types are completely independent:
id!;
id!;
let user_id = generate;
let order_id = generate;
// β
Type-safe: These cannot be accidentally mixed
process_user; // β Compile error!
Instance-Based Generation
Create generators bound to specific worker/process IDs for distributed systems:
id!;
// Server-based: worker ID represents physical/virtual server
let server_15 = worker?;
let id = server_15.generate;
// Process-based: process ID for multi-process applications
let process_7 = process?;
let id = process_7.generate;
// Full control: assign both worker and process IDs
// Example: worker=region, process=datacenter
let us_east_dc2 = instance?;
let id = us_east_dc2.generate;
[!NOTE] State Sharing: Generator instances for the same (worker_id, process_id) pair share the same underlying atomic state. This makes it safe and efficient to create multiple generators for the same IDs across different threads or contextsβthey coordinate through shared state without duplication.
[!TIP] For containerized deployments (Kubernetes, Docker), use Global Defaults to configure worker/process IDs from environment variables. This eliminates the need to pass IDs throughout your application.
Configuration
Presets
Use battle-tested configurations:
use ;
// Config presets (BitLayout + Epoch)
id!; // 42t|10w|0p|12s, epoch: Nov 2010
id!; // 42t|5w|5p|12s, epoch: Jan 2015
// BitLayout presets (use with custom epoch)
TWITTER; // 42t|10w|0p|12s - 1024 workers, 4096 IDs/ms per worker
DISCORD; // 42t|5w|5p|12s - 1024 instances, 4096 IDs/ms per instance
DEFAULT; // Same as DISCORD
// Epoch presets
TWITTER; // Nov 4, 2010 01:42:54 UTC
DISCORD; // Jan 1, 2015 00:00:00 UTC
DEFAULT; // Jan 1, 2025 00:00:00 UTC
[!TIP] New projects: Use a custom epoch near your launch date to maximize capacity. See Choosing an Epoch below.
Custom Configuration
use ;
// Create custom bit allocation
const CUSTOM_CONFIG: Config = new_unchecked;
id!;
Choosing an Epoch
Recommended for new projects: Set your epoch near to your project's launch date.
// Recommended: Set epoch near to your actual launch date
const CONFIG: Config = new_unchecked;
// Suboptimal: Using old preset epochs
const CONFIG: Config = DISCORD; // Epoch from 2015
// This approach consumes years of timestamp capacity before your project even existed
Why this matters:
- β Maximizes your timestamp lifespan starting from when you actually need it
- β Keeps timestamp values smaller during your project's early years
- β Aligns IDs with your project timeline
[!CAUTION] Only change your epoch if you're absolutely certain no IDs have been generated in production yet. Otherwise, keep your current epochβcompatibility with existing IDs is more important than reclaiming unused years.
Global Defaults
In distributed systems (microservices, Kubernetes, multi-region), each service instance typically has the same worker/process ID throughout its lifecycle. Global defaults eliminate the need to pass these IDs aroundβset them once at startup, then use the simple generate() API everywhere.
[!IMPORTANT] Global defaults should be set once at application startup before generating any IDs. They cannot be changed after initialization.
Without global defaults - must create instances:
let generator = instance?;
let id = generator.generate; // Repeat for every service
With global defaults - set once, use everywhere:
use Config;
id!;
id!;
Component Access
id!;
let id = generate;
// Decompose to tuple
let = id.decompose;
// Components struct
let components = id.components;
println!;
// Individual accessors
let timestamp = id.timestamp;
let worker = id.worker_id;
let process = id.process_id;
let sequence = id.sequence;
Composition & Conversions
Compose IDs from Components
id!;
// Compose with default worker/process (validated)
let id = compose?;
// Compose with all components (validated)
let id = compose_custom?;
// Unchecked variants (masks overflow, better performance)
let id = compose_unchecked;
let id = compose_custom_unchecked;
u64 Conversions
let id = generate;
// To u64
let raw: u64 = id.as_u64;
let raw: u64 = id.into;
// From u64 (validated - use for external data)
let id = try_from_u64?;
let id: UserId = raw.try_into?;
// From u64 (unchecked - use for trusted sources)
let id = from_u64_unchecked;
String Conversions
let id = generate;
// To string
let s = id.to_string;
println!;
// From string
let parsed: UserId = s.parse?;
assert_eq!;
JSON Serialization (Serde)
Enable the serde feature for JSON serialization:
[]
= { = "0.1", = ["serde"] }
IDs serialize as strings (not numbers) for safe cross-language compatibility:
use ;
id!;
let user = User ;
let json = to_string_pretty?;
JSON output:
[!TIP] Why strings? JSON numbers are typically parsed as IEEE 754 double-precision floats, which safely represent integers up to 53 bits. Snowflake IDs are 64-bit, so values above
9_007_199_254_740_991lose precision when parsed as numbers. String serialization ensures safe transmission across languages (JavaScript, Python, Java, Go, etc.) and web APIs without data loss.
Architecture
TypedFlake uses a newtype-driven architecture where each ID type maintains completely independent state:
βββββββββββββββββββββββββββββββββββββββββββ
β typedflake::id!(UserId) β
β β
β βββββββββββββββββββββββββββββββββββββββ β
β β Static IdContext (OnceLock) β β
β β β β
β β βββββββββββββββ βββββββββββββββββ β β
β β β Config β β StatePool β β β
β β β (BitLayout, β β (DashMap) β β β
β β β Epoch) β β β β β
β β βββββββββββββββ βββββ¬ββββββββββββ β β
β β β β β
β β ββββββββββββββ΄βββββββββ β β
β β β Lazy State Creation β β β
β β β Arc<AtomicU64> β β β
β β β (worker, process) β β β
β β βββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββ
Key design:
- Per-type isolation: Each
typedflake::id!(TypeName)creates a separate static context - Lock-free generation: Atomic compare-and-swap operations on packed u64 state
- Lazy allocation: States created on-demand per (worker, process) pair using DashMap and shared across all generators for that pair
ID Structure
A TypedFlake ID is a 64-bit integer divided into four components:
| Component | Bits | Range | Description |
|---|---|---|---|
| Timestamp | 42 | 0 - 4,398,046,511,103 | Milliseconds since epoch |
| Worker ID | 5 | 0 - 31 | Worker identifier |
| Process ID | 5 | 0 - 31 | Process identifier |
| Sequence | 12 | 0 - 4,095 | IDs/ms (per instance) |
Default: 42t|5w|5p|12s = 139 years, 1024 instances, 4096 IDs/ms per instance
Total system capacity scales with instances: 1024 instances Γ 4096 IDs/ms = 4,194,304 IDs/ms
Bit allocation is fully customizable:
new; // High-throughput: 16,384 IDs/ms per instance
new; // Long-lived: ~1,115 years
Performance
TypedFlake is designed for high-throughput scenarios:
- Lock-free: Atomic compare-and-swap operations with no mutexes
- Zero allocations: ID generation doesn't allocate memory
- Cache-friendly: Packed atomic state with cache-line alignment
- Lazy initialization: Only allocates state for actively-used instances
Run benchmarks:
License
Acknowledgments
Algorithm inspirations:
- Twitter Snowflake (original algorithm)
- Discord's Snowflake implementation
Design philosophy: