#Design a Unique ID Generator (Twitter Snowflake Scale)

#1. Problem Statement & Clarifications

A distributed unique ID generator produces globally unique, roughly time-ordered 64-bit IDs without requiring coordination between nodes. This is a foundational building block β€” every distributed system needs unique identifiers for database records, events, messages, and transactions. The challenge is generating IDs that are unique, sortable, compact, and fast at massive scale.

#Functional Requirements

  1. Generate unique IDs β€” Globally unique across all datacenters and servers, no collisions ever
  2. Time-sortable β€” IDs are roughly ordered by generation time (newer ID > older ID)
  3. 64-bit integer β€” Fits in a standard BIGINT; no strings/UUIDs
  4. High throughput β€” Generate 10K+ IDs/sec per node, 1M+ IDs/sec globally
  5. No coordination β€” Each node generates IDs independently (no distributed locks)

#Non-Functional Requirements

Requirement Target
Latency < 1ms per ID generation (local operation)
Throughput 10K IDs/sec per node; 1M+ globally
Uniqueness Zero collisions across all nodes forever
Sortability IDs generated later are numerically larger (within ~1ms precision)
Availability 99.999% β€” ID generation must never block
Size 64-bit integer (fits in DB BIGINT, language long)

#Out of Scope

#Assumptions


#2. Back-of-Envelope Estimation

#Traffic Estimates

Average ID generation:  100K IDs/sec globally
Peak (10x burst):       1M IDs/sec
Nodes:                  1024 machines across 32 datacenters
Per-node avg:           ~100 IDs/sec (uneven; some nodes much higher)
Per-node peak:          10K IDs/sec (burst on hot services)

#Storage Estimates

ID size:                8 bytes (64-bit integer)
IDs/day:                100K Γ— 86400 = 8.64B IDs/day
Storage for IDs alone:  8.64B Γ— 8B = 69GB/day (trivial; IDs are stored
                        alongside records, not in a separate ID store)

#Bit Budget (64-bit layout)

Total bits:             64
Sign bit:               1 (always 0, keeps IDs positive)
Timestamp:              41 bits β†’ 2^41ms = 69.7 years from epoch
Datacenter ID:          5 bits β†’ 2^5 = 32 datacenters
Machine ID:             5 bits β†’ 2^5 = 32 machines per datacenter
Sequence:               12 bits β†’ 2^12 = 4096 IDs per millisecond per machine
                                 = 4.096M IDs/sec per machine (headroom)

#Capacity Analysis

Max IDs/ms/machine:     4,096 (12-bit sequence)
Max IDs/sec/machine:    4,096,000
Max IDs/sec/cluster:    4,096,000 Γ— 1,024 machines = 4.19B IDs/sec
Lifetime:               69.7 years from custom epoch
                        (custom epoch = Jan 1, 2025 β†’ expires ~2094)

#3. API Design

#Generate Single ID

GET /api/v1/id/generate

Response 200:
{
  "id": 7109812942348288001,
  "id_hex": "62B5D2E400001001",
  "timestamp": 1716984000123,
  "datacenter_id": 5,
  "machine_id": 12,
  "sequence": 1
}

#Generate Batch IDs

POST /api/v1/id/generate/batch
{
  "count": 100
}

Response 200:
{
  "ids": [7109812942348288001, 7109812942348288002, ...],
  "count": 100,
  "generated_at": "2025-05-29T12:00:00.123Z"
}

#Parse ID (Debug/Introspection)

GET /api/v1/id/parse/7109812942348288001

Response 200:
{
  "id": 7109812942348288001,
  "timestamp_ms": 1716984000123,
  "timestamp_iso": "2025-05-29T12:00:00.123Z",
  "datacenter_id": 5,
  "machine_id": 12,
  "sequence": 1
}

#Health / Status

GET /api/v1/id/status

Response 200:
{
  "datacenter_id": 5,
  "machine_id": 12,
  "ids_generated_total": 1234567890,
  "ids_generated_last_sec": 342,
  "clock_status": "synchronized",
  "uptime_seconds": 864000
}

#4. Data Model

#ID Bit Layout (Snowflake Structure)

β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Sign β”‚           Timestamp (ms)             β”‚ DC  β”‚Mach β”‚  Sequence  β”‚
β”‚ 1b  β”‚              41 bits                 β”‚ 5b  β”‚ 5b  β”‚   12 bits  β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  0      [custom epoch offset in ms]          [0-31][0-31]  [0-4095]

Example ID: 7109812942348288001
Binary: 0 | 11000101011010111010010111001000000000001 | 00101 | 01100 | 000000000001

Decoded:
  Timestamp: 1716984000123 ms since custom epoch (2025-01-01)
  Datacenter: 5
  Machine: 12
  Sequence: 1

#Machine Registration (PostgreSQL or ZooKeeper β€” control plane only)

CREATE TABLE machine_registry (
  datacenter_id   SMALLINT NOT NULL,
  machine_id      SMALLINT NOT NULL,
  hostname        VARCHAR(255) NOT NULL,
  ip_address      INET NOT NULL,
  registered_at   TIMESTAMP DEFAULT NOW(),
  last_heartbeat  TIMESTAMP DEFAULT NOW(),
  is_active       BOOLEAN DEFAULT true,
  PRIMARY KEY (datacenter_id, machine_id)
);

CREATE INDEX idx_machine_heartbeat ON machine_registry(last_heartbeat)
  WHERE is_active = true;

#No Hot-Path Data Store

The ID generator has NO hot-path database dependency.
- ID generation is purely in-memory (bitwise operations)
- Machine registration is a one-time startup operation
- No reads or writes to any external store during ID generation
- This is what makes it fast (< 1ΞΌs per ID)

#Access Patterns

Query Store Frequency
Generate ID In-memory (no store) Every request (hot path)
Register machine on startup PostgreSQL / ZooKeeper Once per boot
Heartbeat update PostgreSQL Every 30 seconds
Parse/decode ID In-memory bit manipulation Debug only

#5. High-Level Design (HLD) & Scale Evolution

#Stage 1: MVP / Single Server (1K IDs/sec)

#Stage 2: Growth Scale (100K IDs/sec, 10 nodes)

#Stage 3: Twitter Snowflake Scale (1M+ IDs/sec, 1024 nodes)

#Architecture Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Datacenter 1 (dc_id=0)                     β”‚
β”‚                                                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚  β”‚ App Server 1 β”‚ β”‚ App Server 2 β”‚ β”‚ App Server N β”‚             β”‚
β”‚  β”‚ (machine=0)  β”‚ β”‚ (machine=1)  β”‚ β”‚ (machine=31) β”‚             β”‚
β”‚  β”‚              β”‚ β”‚              β”‚ β”‚              β”‚             β”‚
β”‚  β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚             β”‚
β”‚  β”‚ β”‚ ID Gen   β”‚ β”‚ β”‚ β”‚ ID Gen   β”‚ β”‚ β”‚ β”‚ ID Gen   β”‚ β”‚             β”‚
β”‚  β”‚ β”‚ Library  β”‚ β”‚ β”‚ β”‚ Library  β”‚ β”‚ β”‚ β”‚ Library  β”‚ β”‚             β”‚
β”‚  β”‚ β”‚ (in-proc)β”‚ β”‚ β”‚ β”‚ (in-proc)β”‚ β”‚ β”‚ β”‚ (in-proc)β”‚ β”‚             β”‚
β”‚  β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚                         β”‚                                       β”‚
β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                            β”‚
β”‚              β”‚  Machine Registry    β”‚                            β”‚
β”‚              β”‚  (ZooKeeper / etcd)  β”‚                            β”‚
β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Datacenter 2 (dc_id=1)                     β”‚
β”‚              (Same structure, different dc_id)                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    ... up to 32 datacenters

#Component Breakdown

Component Responsibility Tech Choice
ID Generator Library In-process ID generation; bit manipulation; sequence management Go / Java / Rust library
Machine Registry Assign unique (dc_id, machine_id) pairs; prevent duplicates ZooKeeper / etcd / PostgreSQL
NTP Service Clock synchronization across all machines ntpd / chrony
Monitoring Agent Track ID generation rate, clock drift, sequence overflow Prometheus + Grafana

#Data Flow

ID Generation (The Hot Path β€” entirely in-process):

Application calls idgen.next_id()
  β†’ 1. Get current timestamp in milliseconds
  β†’ 2. If same millisecond as last ID:
        sequence++ (up to 4095)
        If sequence overflows β†’ wait until next millisecond
  β†’ 3. If new millisecond:
        sequence = 0
  β†’ 4. Assemble 64-bit ID:
        id = (timestamp << 22) | (dc_id << 17) | (machine_id << 12) | sequence
  β†’ 5. Return id (zero network calls, < 1ΞΌs)

Machine Registration (Startup only):

App server boots β†’ ID Gen Library init
  β†’ 1. Read dc_id from environment/config
  β†’ 2. Request machine_id from ZooKeeper (ephemeral node)
     OR read from config file (static assignment)
  β†’ 3. Verify uniqueness: no other active node has same (dc_id, machine_id)
  β†’ 4. Start heartbeat (every 30s)
  β†’ 5. Ready to generate IDs

#6. Deep Dive β€” Core Components

#Snowflake ID Generator β€” Detailed Design

Why 64-bit Snowflake over alternatives?

Approach Size Sortable Coordination IDs/sec/node Drawbacks
UUID v4 (random) 128-bit No None Unlimited Too large for DB index; not sortable
UUID v7 (time) 128-bit Yes None Unlimited 128-bit wastes space; string format
DB auto-increment 64-bit Yes Per-DB ~10K Single DB bottleneck; coordination required
DB ticket server 64-bit Yes Per-ticket-server ~50K Still centralized; SPOF
Snowflake 64-bit Yes None (runtime) 4M Machine ID assignment needed at boot

Chosen: Snowflake β€” Best balance of size, sortability, throughput, and independence.

#Clock Management β€” Detailed Design

The Clock Skew Problem:

If server clock goes BACKWARD (NTP adjustment, leap second):
  β†’ Same timestamp reused β†’ potential duplicate IDs!

Mitigation:
  1. Track last_timestamp in memory
  2. If current_time < last_timestamp β†’ clock went backward
  3. Wait until clock catches up (spin-wait)
  4. If backward > 5ms β†’ alert + refuse to generate (prevent long stalls)
  5. Use monotonic clock where possible (clock_gettime(MONOTONIC))

NTP Best Practices:

- Use multiple NTP servers (pool.ntp.org + internal stratum 1)
- Chrony preferred over ntpd (faster convergence, slew not step)
- Max allowed drift: 10ms (beyond this β†’ alert)
- Leap second handling: Google's leap smear (spread over 24h)

#Machine ID Assignment β€” Detailed Design

Three strategies:

Strategy Pros Cons
Static config Simplest; no dependency Manual management; error-prone
ZooKeeper ephemeral nodes Auto-release on crash; no duplicates ZK dependency at startup
Database lease No ZK needed; familiar Must renew lease; DB as dependency

Chosen: ZooKeeper ephemeral nodes (for large deployments)

On startup:
  1. Try to create /id-gen/dc/{dc_id}/machine/{0..31} (ephemeral)
  2. First available slot β†’ that's your machine_id
  3. Node crash β†’ ephemeral node auto-deleted β†’ slot freed
  4. Heartbeat: ZK session keepalive handles this automatically

#Scaling Strategy

Component Strategy
ID generation Pure in-process; scales with app servers (no bottleneck)
Machine registry ZooKeeper 3-node quorum; only used at boot
Multi-datacenter Each DC gets unique dc_id (5 bits = 32 DCs)
Beyond 1024 machines Expand bit layout (steal from timestamp or sequence)

#Caching Strategy

N/A β€” ID generation is entirely in-memory.
No caching needed. No external data dependencies on hot path.
The generator IS the cache: it holds only:
  - last_timestamp (8 bytes)
  - sequence counter (2 bytes)
  - dc_id + machine_id (2 bytes)
Total state: 12 bytes in memory.

#Consistency Models

Data Model Rationale
Generated IDs Globally unique (no consistency needed) Each node generates from its own bit-space; no overlap possible
Machine registry Strongly consistent (ZooKeeper) Must prevent duplicate (dc_id, machine_id) assignments
Clock Eventually consistent (NTP) ~10ms drift acceptable; IDs from different nodes not strictly ordered
Time ordering Roughly ordered (within ~10ms) IDs from same node are strictly ordered; cross-node within NTP drift

#7. Low-Level Design β€” Core Functionality

#Key Algorithms

1. Snowflake ID Generator

import time
import threading

class SnowflakeGenerator:
    # Bit allocation
    TIMESTAMP_BITS = 41
    DC_BITS = 5
    MACHINE_BITS = 5
    SEQUENCE_BITS = 12

    MAX_SEQUENCE = (1 << SEQUENCE_BITS) - 1       # 4095
    MAX_MACHINE = (1 << MACHINE_BITS) - 1          # 31
    MAX_DC = (1 << DC_BITS) - 1                    # 31

    # Bit shifts
    TIMESTAMP_SHIFT = DC_BITS + MACHINE_BITS + SEQUENCE_BITS  # 22
    DC_SHIFT = MACHINE_BITS + SEQUENCE_BITS                    # 17
    MACHINE_SHIFT = SEQUENCE_BITS                               # 12

    # Custom epoch: Jan 1, 2025 00:00:00 UTC
    EPOCH = 1735689600000  # ms

    def __init__(self, datacenter_id, machine_id):
        assert 0 <= datacenter_id <= self.MAX_DC
        assert 0 <= machine_id <= self.MAX_MACHINE
        self.dc_id = datacenter_id
        self.machine_id = machine_id
        self.sequence = 0
        self.last_timestamp = -1
        self.lock = threading.Lock()

    def _current_millis(self):
        return int(time.time() * 1000)

    def _wait_next_millis(self, last_ts):
        """Spin-wait until clock advances to next millisecond."""
        ts = self._current_millis()
        while ts <= last_ts:
            ts = self._current_millis()
        return ts

    def next_id(self):
        with self.lock:
            timestamp = self._current_millis()

            # Clock went backward β€” critical error
            if timestamp < self.last_timestamp:
                drift = self.last_timestamp - timestamp
                if drift > 5:  # > 5ms backward
                    raise ClockDriftError(f"Clock moved back {drift}ms")
                # Small drift: wait it out
                timestamp = self._wait_next_millis(self.last_timestamp)

            if timestamp == self.last_timestamp:
                # Same millisecond: increment sequence
                self.sequence = (self.sequence + 1) & self.MAX_SEQUENCE
                if self.sequence == 0:
                    # Sequence overflow: wait for next millisecond
                    timestamp = self._wait_next_millis(self.last_timestamp)
            else:
                # New millisecond: reset sequence
                self.sequence = 0

            self.last_timestamp = timestamp
            ts_offset = timestamp - self.EPOCH

            # Assemble 64-bit ID
            id = ((ts_offset << self.TIMESTAMP_SHIFT) |
                  (self.dc_id << self.DC_SHIFT) |
                  (self.machine_id << self.MACHINE_SHIFT) |
                  self.sequence)

            return id

2. ID Parser (Decode)

def parse_snowflake_id(id_value):
    """Decode a Snowflake ID into its components."""
    EPOCH = 1735689600000

    sequence = id_value & 0xFFF                    # last 12 bits
    machine_id = (id_value >> 12) & 0x1F           # next 5 bits
    dc_id = (id_value >> 17) & 0x1F                # next 5 bits
    timestamp = (id_value >> 22) + EPOCH            # remaining 41 bits

    return {
        "timestamp_ms": timestamp,
        "timestamp_iso": datetime.fromtimestamp(timestamp/1000).isoformat(),
        "datacenter_id": dc_id,
        "machine_id": machine_id,
        "sequence": sequence
    }

3. Machine ID Auto-Assignment (ZooKeeper)

def acquire_machine_id(zk_client, datacenter_id):
    """Acquire a unique machine_id via ZooKeeper ephemeral node."""
    for machine_id in range(32):  # 0-31
        path = f"/id-gen/dc/{datacenter_id}/machine/{machine_id}"
        try:
            # Create ephemeral node β€” auto-deleted on disconnect
            zk_client.create(path, ephemeral=True,
                           value=socket.gethostname().encode())
            return machine_id  # successfully claimed
        except NodeExistsError:
            continue  # slot taken, try next

    raise NoAvailableSlots(f"All 32 machine IDs taken in DC {datacenter_id}")

#Design Patterns Used

Pattern Where Why
Embedded Library ID gen as in-process library, not a service Zero network latency; no SPOF; highest throughput
Bit Packing 64-bit ID with timestamp + DC + machine + seq Compact; sortable; self-describing
Lease-based Assignment ZK ephemeral nodes for machine IDs Auto-release on crash; no manual cleanup
Monotonic Counter Sequence number within same millisecond Guarantees uniqueness within same ms on same machine

#8. Failure Handling & Edge Cases

#What Happens When X Fails?

Failure Impact Mitigation
Clock goes backward Potential duplicate IDs Detect and spin-wait for small drift (< 5ms); error for large drift
NTP outage Clock drift increases over time Local oscillator holds; alert if drift > 10ms; continue generating
ZooKeeper down Can't register NEW nodes Running nodes unaffected (machine_id already assigned); new boots fail
Machine crashes Ephemeral ZK node deleted; machine_id freed New process acquires same or different slot; no duplicate risk
Sequence overflow > 4096 IDs in 1ms on one machine Wait for next millisecond (< 1ms delay); alert on sustained overflow
Datacenter failure All IDs from that DC stop Other DCs unaffected; IDs are globally unique across DCs

#Edge Cases


#9. Monitoring & Observability

#Key Metrics

Metric Target Alert Threshold
IDs generated/sec Varies by service > 4000/ms (approaching sequence overflow)
Clock drift from NTP < 10ms > 50ms
Sequence overflow events 0 > 10/min
Clock backward events 0 > 1/hour
ZK session health Connected Disconnected > 30s
Machine ID conflicts 0 > 0 (critical)

#Alerting Strategy

#SLAs / SLOs

ID Generation Latency:  P99 < 1ms (typically < 1ΞΌs)
Uniqueness:             100% (zero collisions, by design)
Availability:           99.999% (in-process; no external dependency on hot path)
Sortability:            Monotonic within same machine; Β±10ms across machines

#10. Trade-off Summary

Decision Chose Over Because
ID format 64-bit Snowflake UUID (128-bit) Half the size; DB-friendly BIGINT; time-sortable
Architecture In-process library Standalone ID service Zero latency; no network dependency; no SPOF
Timestamp precision Milliseconds Microseconds 41 bits covers 69 years; ΞΌs would need more bits
Coordination Startup-only (ZK) Runtime coordination Hot path has zero external dependencies
Clock drift handling Spin-wait + error Accept duplicates Correctness over availability for ID uniqueness
Sequence overflow Wait for next ms Pre-allocate ranges Simpler; overflow rare (4096 IDs/ms is high)
Bit allocation 41-5-5-12 Custom split Proven by Twitter; good balance for most deployments
Custom epoch Jan 1, 2025 Unix epoch (1970) Saves 55 years of timestamp space; extends lifetime to 2094

#11. Extensions & Follow-ups

#What Would You Add With More Time?

  1. UUID v7 Compatibility β€” Optional 128-bit mode for systems requiring UUID format
  2. Multi-Tenant ID Spaces β€” Embed tenant ID in the bit layout for multi-tenant SaaS
  3. ID Reservation β€” Pre-allocate ID ranges for batch operations (e.g., bulk import)
  4. Encryption β€” Optionally encrypt IDs to hide timestamp/machine info from external users
  5. Metrics Dashboard β€” Real-time visualization of ID generation rates across all nodes
  6. Backward-Compatible Migration β€” Tool to migrate from auto-increment to Snowflake IDs

#How Would This Change at 100x Scale?


#12. Cross-References

Topic Connection
URL Shortener (#1) Key Gen Service is a Snowflake-like ID generator
Key-Value Store (#3) KV stores use unique IDs for internal coordination
Pastebin (#25) Pre-generated key ranges solve the same uniqueness problem

#Building Blocks Used

#Concepts Used