UUIDs (or GUIDs) are everywhere: databases, logs, messages, tokens. But what if you want to go beyond off-the-shelf libraries and build your own custom GUID generator — tuned for your system’s needs?
With Rust’s low-level performance, strong type system, and safety guarantees, it’s an ideal language for rolling your own.
Let’s build a blazing-fast GUID generator in Rust — from first principles to a high-performance implementation.
🧠 Why Build a Custom GUID?
Use cases for custom GUIDs include:
- Shorter identifiers (not always 128 bits)
- Lexicographically sortable IDs (timestamp-first)
- Domain-specific encodings (e.g. shard ID + timestamp + random)
- Reduced collisions in high-throughput systems
- Cryptographically opaque yet deterministic IDs
UUIDv4 and UUIDv7 are great, but not always a perfect fit. Sometimes you want just enough structure to work for your architecture.
📐 Design: Our Custom GUID Format
We'll build a 128-bit (16-byte) identifier with the following layout:
Bits | Field | Description |
---|---|---|
48 | Timestamp | Milliseconds since Unix epoch |
16 | Shard ID | Useful for data locality |
64 | Randomness | High entropy component |
This format is:
- Globally unique
- Time-sortable
- Shard-aware (good for multi-node systems)
🛠️ Crate Setup
In your Cargo.toml
:
[dependencies]
rand = "0.8"
once_cell = "1.17"
🧱 Implementation
use rand::{rngs::ThreadRng, RngCore};
use std::time::{SystemTime, UNIX_EPOCH};
/// 128-bit GUID
#[derive(Debug, Clone, Copy)]
pub struct Guid([u8; 16]);
impl Guid {
pub fn new(shard_id: u16, rng: &mut ThreadRng) -> Self {
let mut bytes = [0u8; 16];
// Timestamp (48 bits = 6 bytes)
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
bytes[0..6].copy_from_slice(×tamp.to_be_bytes()[2..]);
// Shard ID (16 bits = 2 bytes)
bytes[6..8].copy_from_slice(&shard_id.to_be_bytes());
// Random bytes (64 bits = 8 bytes)
rng.fill_bytes(&mut bytes[8..16]);
Guid(bytes)
}
pub fn to_hex(&self) -> String {
self.0.iter().map(|b| format!("{:02x}", b)).collect()
}
}
🔁 Example Usage
fn main() {
let mut rng = rand::thread_rng();
for shard in 0..2 {
let guid = Guid::new(shard, &mut rng);
println!("Shard {} GUID: {}", shard, guid.to_hex());
}
}
⚡ Performance Optimizations
1. Use thread-local RNGs for speed and concurrency safety.
2. Avoid system calls: don’t query system time repeatedly unless necessary.
3. Pack bytes directly — avoid strings or hex unless needed.
4. Batch generate if your system needs 100k+ GUIDs per second.
🧪 Advanced: Monotonic Timestamps
To prevent issues from system clock skews, especially in UUIDv7-style sortable IDs, you can track the last timestamp and increment locally if needed:
use std::sync::atomic::{AtomicU64, Ordering};
use once_cell::sync::Lazy;
static LAST_TS: Lazy<AtomicU64> = Lazy::new(|| AtomicU64::new(0));
fn monotonic_ts() -> u64 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let mut last = LAST_TS.load(Ordering::Relaxed);
if now > last {
LAST_TS.store(now, Ordering::Relaxed);
now
} else {
LAST_TS.fetch_add(1, Ordering::Relaxed) + 1
}
}
📦 Where to Use Custom GUIDs
- High-throughput event logs
- Globally sharded databases
- Distributed caches
- Custom protocol IDs
- Link shortening and QR encoding
🔐 Security Considerations
- Do not use these for tokens, passwords, or secrets without additional encryption or signing
- You can hash or encrypt GUIDs if obfuscation is required
- Consider truncating to 64 bits only if your domain guarantees low collision
Final Thoughts
Rolling your own GUID generator can offer:
- Better performance
- Smaller size
- Domain-specific encodings
- Built-in sorting or sharding semantics
Rust gives you the safety and performance you need to do it confidently.
⚙️ Identifiers don’t have to be generic — and when you control the generator, you control the universe (or at least your logs).