⚡ sparkid
Fast, monotonic, time-sortable, 21-char Base58 unique ID generator. Only dependency is rand.
1ocmpHE1bFnygEBAPTzMK
1ocmpHE1bFnygFv4Wp4dL
1ocmpHE1bFnygGoUXUL7X
Install
Usage
use SparkId;
let id = new;
// => "1ocmpHE1bFnygEBAPTzMK"
println!; // Display, no heap allocation
let s = id.as_str; // SparkIdStr — stack-allocated, Deref<str>
let owned: String = id.into; // Into<String> when needed
Properties
| Property | Value |
|---|---|
| Length | 21 characters, fixed |
| Alphabet | Base58 (no 0, O, I, l) |
| Sortable | Lexicographically, by creation time |
| Monotonic | Strictly increasing within each thread |
| URL-safe | Yes |
| Collision resistance | ~5813 (~8.4 x 1022) combinations per millisecond |
| Randomness | Cryptographically secure (rand / ChaCha12, seeded from OS) |
| Thread-safe | Yes (via thread_local!) |
How it works
Each ID is composed of two parts:
[8-char timestamp][13-char suffix]
- Timestamp (8 chars): Current time in milliseconds, Base58-encoded. IDs generated in a later millisecond always sort after earlier ones.
- Suffix (13 chars): Seeded from a cryptographically secure PRNG (rejection-sampled, no modulo bias) at the start of each millisecond, then monotonically incremented for each subsequent ID within that millisecond. This guarantees strict ordering even when multiple IDs share a timestamp.
SparkId type
SparkId is a stack-allocated, Copy type backed by a u128 — no heap allocation on creation. The 21 Base58 characters are bit-packed into 128 bits (6 bits per character), preserving sort order. It implements Display, Ord, Hash, FromStr, and Into<String>.
Use as_str() to get a SparkIdStr — a stack-allocated, Copy wrapper that dereferences to &str:
use SparkId;
let a = new;
let b = new;
assert!; // Ord — monotonically increasing
println!; // Display — no allocation
let s = a.as_str; // SparkIdStr — Deref<str>, no heap
let slice: &str = &s; // zero-cost &str access
let set: HashSet = .into; // Hash
Parse from string
use SparkId;
let id = new;
let parsed: SparkId = id.to_string.parse.unwrap;
assert_eq!;
Parsing validates that the input is exactly 21 characters and all characters are in the Base58 alphabet. Returns a ParseSparkIdError on failure.
Binary representation
SparkId is backed by a u128 (16 bytes) — smaller and faster to compare/sort than the 21-char string form. You can serialize the binary representation directly:
use SparkId;
let id = new;
// 16-byte big-endian binary — sort-preserving, memcmp-comparable
let bytes: = id.to_bytes;
let restored = from_bytes.unwrap;
assert_eq!;
// Raw u128 — branchless comparison, ideal for in-memory sorting
let raw: u128 = id.as_u128;
let also_restored = from_u128.unwrap;
assert_eq!;
Extract timestamp
use SparkId;
let id = new;
// As milliseconds since epoch (available in no_std)
let ms = id.timestamp_ms;
// As SystemTime (requires std)
let ts = id.timestamp;
Serde
Enable the serde feature for Serialize and Deserialize on both SparkId and SparkIdStr:
Human-readable formats (JSON, TOML, etc.) serialize as the 21-char Base58 string. Binary formats (postcard, bincode, etc.) serialize as the 16-byte packed representation.
use SparkId;
Ordering guarantees
IDs from a single IdGenerator instance (or a single thread using SparkId::new()) are strictly monotonically increasing — every ID is lexicographically greater than the one before it.
Across threads, IDs are unique but not ordered relative to each other. Each thread gets its own generator via thread_local!, so there is no cross-thread coordination.
If you need process-wide monotonic ordering across threads, wrap a single IdGenerator in a Mutex:
use ;
use IdGenerator;
static GEN: =
new;
Advanced usage
For manual control, use the IdGenerator struct directly:
use IdGenerator;
let mut gen = new;
let id = gen.next_id;
IdGenerator also implements Iterator<Item = SparkId>:
let mut gen = new;
let ids: = gen.take.collect;
Performance
License
MIT