Building a Custom GUID Generator in Rust

    July 8, 2024
    10 min read
    Long read
    Tutorial
    uuid
    code-examples
    innovation

    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:

    BitsFieldDescription
    48TimestampMilliseconds since Unix epoch
    16Shard IDUseful for data locality
    64RandomnessHigh entropy component

    This format is:

    • Globally unique
    • Time-sortable
    • Shard-aware (good for multi-node systems)

    🛠️ Crate Setup

    In your Cargo.toml:

    toml
    [dependencies]
    rand = "0.8"
    once_cell = "1.17"

    🧱 Implementation

    rust
    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(&timestamp.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

    rust
    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:

    rust
    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).

    Generate Your Own UUIDs

    Ready to put this knowledge into practice? Try our UUID generators:

    Generate a Single UUID

    Create a UUID with our fast, secure generator

    Bulk UUID Generator

    Need multiple UUIDs? Generate them in bulk

    Summary

    This detailed Rust tutorial walks through building a custom GUID generator from scratch. Learn the algorithm, performance techniques, and implementation patterns for generating UUID-like identifiers efficiently in Rust.

    TLDR;

    Learn how to build a high-performance custom GUID generator in Rust using low-level control and modern best practices.

    Key takeaways:

    • GUIDs can be generated using timestamps, entropy, and well-defined bit layouts
    • Rust offers speed, safety, and concurrency advantages for identifier generation
    • A custom GUID design lets you tailor format, sortability, or size to your needs

    Ideal for systems requiring ultra-fast, traceable, or domain-specific identifiers.

    Cookie Consent

    We use cookies to enhance your experience on our website. By accepting, you agree to the use of cookies in accordance with our Privacy Policy.