Design a Unique Distributed ID Generator (Snowflake-Style)
Generate 64-bit unique IDs at scale: Twitter Snowflake 64-bit binary layout (1-bit sign, 41-bit timestamp, 10-bit worker ID, 12-bit sequence), time-sortability, and clock drift handling.
Twitter Snowflake 64-Bit Distributed ID Generator โ๏ธ
Structure of 64-bit time-sortable IDs: 1-bit sign, 41-bit timestamp, 10-bit machine ID, and 12-bit sequence, optimized for B+ tree index append performance.
01.1. Functional & Non-Functional Requirements
A Distributed ID Generator generates unique, monotonically increasing identifiers across hundreds of distributed database shards and microservices without central lock contention.
Functional Requirements
- Global Uniqueness: Every generated ID must be globally unique across all servers and datacenters.
- Time-Ordered (Roughly Monotonic): IDs generated at time
T_2 > T_1must have numerical valuesID_2 > ID_1, allowing sorting directly by ID. - Compact Size: Fits in a standard 64-bit integer (
BIGINTin SQL /int64in Go/Java) to conserve index RAM and storage.
Non-Functional Requirements
- Massive Throughput: Must generate
> 100,000 IDs/secper individual worker node in local CPU memory with zero network roundtrips. - Zero Central Coordination: Generation must not depend on a central database lock or coordinator for each individual ID.
- High Availability: If network partitions occur between datacenters, local nodes continue generating valid unique IDs without interruption.
02.2. Why Random UUIDv4 Fails for Database Primary Keys
A common junior developer mistake is using 128-bit random UUIDv4 (f47ac10b-58cc-4372-a567-0e02b2c3d479) for database primary keys.
The Physics of B+ Tree Index Degradation:
- Random Insertion Page Splits: Relational databases (PostgreSQL, MySQL InnoDB) store rows physically ordered by primary key in a clustered B+ Tree. Because UUIDv4 is completely random, new records insert randomly throughout the tree, causing frequent B+ Tree page splits, excessive write amplification, and random disk I/O.
- Memory Bloat: 128-bit UUID strings (36 characters) consume more than double the RAM and disk of a 64-bit integer. Index caches fill up rapidly, dropping buffer pool hit ratios.
- Snowflake Solution: Because Snowflake IDs are roughly monotonically increasing based on timestamp, new database rows append sequentially to the rightmost leaf of the B+ Tree, completely eliminating page splits and achieving maximum write IOPS.
03.3. The 64-Bit Snowflake Binary Structure
Twitter Snowflake divides a 64-bit signed integer into 4 distinct binary bit fields:
code+---+-------------------------------------------+--------------+--------------+ | 1 | 41 Bits: Timestamp (Milliseconds) | 10 Bits: Node| 12 Bits: Seq | +---+-------------------------------------------+--------------+--------------+ | | | | Unused (0) Epoch Offset (ms) Worker ID Sequence (Sign Bit) (Supports ~69.7 Years) (0 - 1023) (0 - 4095/ms)
Bit Breakdown & Math:
- 1 Bit (Sign Bit): Always
0to ensure the generated 64-bit number is a positive integer in Java, Go, and SQLBIGINT. - 41 Bits (Timestamp): Milliseconds elapsed since a custom epoch (e.g.,
2026-01-01T00:00:00Z=1767225600000ms):
2^{41} = 2,199,023,255,552 ms โ 69.73 Years of Lifetime
- 10 Bits (Machine / Worker ID): Uniquely identifies up to
2^{10} = 1,024 worker nodes(often divided into 5-bit Datacenter ID = 32 datacenters, and 5-bit Worker ID = 32 nodes per DC). - 12 Bits (Sequence Counter): Increments for each ID generated within the same millisecond on the same node:
2^{12} = 4,096 unique IDs per millisecond per node
Maximum Cluster Throughput:
Throughput = 1,024 nodes ร 4,096 IDs/ms ร 1,000 ms/sec = 4.19 Billion IDs/second!
04.4. Implementation Code (TypeScript / JavaScript)
Here is a production-grade implementation of a Snowflake generator using 64-bit BigInt:
typescriptexport class SnowflakeIdGenerator { private readonly epoch: bigint = 1767225600000n; // Custom Epoch (Jan 1, 2026) private readonly workerIdBits: bigint = 10n; private readonly sequenceBits: bigint = 12n; private readonly maxWorkerId: bigint = (1n << this.workerIdBits) - 1n; // 1023 private readonly maxSequence: bigint = (1n << this.sequenceBits) - 1n; // 4095 private readonly timestampShift: bigint = this.workerIdBits + this.sequenceBits; // 22 private readonly workerIdShift: bigint = this.sequenceBits; // 12 private workerId: bigint; private sequence: bigint = 0n; private lastTimestamp: bigint = -1n; constructor(workerId: number) { if (workerId < 0 || BigInt(workerId) > this.maxWorkerId) { throw new Error(`Worker ID must be between 0 and ${this.maxWorkerId}`); } this.workerId = BigInt(workerId); } public nextId(): bigint { let now = BigInt(Date.now()); if (now < this.lastTimestamp) { const offset = this.lastTimestamp - now; if (offset <= 5n) { // Clock drift small: wait until clock catches up while (now < this.lastTimestamp) { now = BigInt(Date.now()); } } else { throw new Error(`NTP Clock moved backwards by ${offset}ms! Refusing to generate ID.`); } } if (now === this.lastTimestamp) { this.sequence = (this.sequence + 1n) & this.maxSequence; if (this.sequence === 0n) { // Sequence exhausted for current ms; wait for next ms while (now <= this.lastTimestamp) { now = BigInt(Date.now()); } } } else { this.sequence = 0n; } this.lastTimestamp = now; return ( ((now - this.epoch) << this.timestampShift) | (this.workerId << this.workerIdShift) | this.sequence ); } }
05.5. Deep Dives: NTP Clock Skew, Worker Assignment & UUIDv7
1. Handling NTP Clock Drift & Backward Jumps
Server clocks periodically synchronize via Network Time Protocol (NTP). If an NTP synchronization steps the local clock backwards (T_{now} < T_{last}), naive generators could generate duplicate IDs.
- Mitigation Strategies:
- Slight Drift (
< 5 ms): Pause execution in a tight spin-lock loop until the clock catches up tolastTimestamp. - Large Drift (
> 5 ms): Refuse generation and trigger an automated alert, or switch to a reserved secondaryworker_idoffset. - Monotonic Clocks: Use monotonic OS clock APIs (
CLOCK_MONOTONIC_RAW) that never step backwards.
- Slight Drift (
2. Machine Worker ID Assignment
How do worker nodes get assigned their 10-bit ID (0-1023)?
- Dynamic ZooKeeper / Consul Registration: On boot, the container creates an ephemeral sequential znode in ZooKeeper (e.g.,
/workers/id_042) to claim worker ID42. When the container dies, the lease expires and is reclaimed.
3. Alternative: UUIDv7 (The Modern RFC 9562 Standard)
- Combines 48-bit millisecond timestamp + 74-bit random entropy.
- Time-ordered like Snowflake, but 128-bit long. Snowflake remains the preferred choice when 64-bit integer performance is critical.
โ๏ธArchitectural Trade-offs & Production Realities
Architectural Advantages
- Generates roughly time-sorted 64-bit integers that append sequentially to database B+ Tree indexes with zero page splits
- Local in-memory generation produces over 4 million IDs/sec with zero network RPC calls
- Fits compactly into standard SQL BIGINT, saving 50% RAM compared to 128-bit UUIDs
Trade-offs & Constraints
- Vulnerable to NTP clock skew (requires backward clock drift guard logic)
- Worker IDs must be coordinated across the cluster (max 1,024 worker nodes per custom epoch)
Discord and Twitter use Snowflake IDs for all messages and tweets. Because IDs encode the creation timestamp directly, client UIs sort chat messages chronologically in real time without querying separate timestamp columns.
๐ฏ Staff+ Engineering Takeaways
- Twitter Snowflake produces 64-bit, time-sortable, globally unique IDs.
- Local in-memory generation achieves millions of IDs per second without network bottlenecks.
- Time-ordered IDs prevent database B+ Tree index page splits and optimize write IOPS.
- Guard against NTP backward clock drift using spin-wait loops or monotonic clock sources.
Topic Knowledge Assessment ๐ง
Step through 2 scenario questions to test your staff-level grasp.
What is the primary operational advantage of Twitter Snowflake 64-bit IDs over random 128-bit UUIDv4 for database primary keys?
How clear and staff-actionable was this system breakdown?