Design a Distributed Unique ID Generator

Snowflake and its rivals: mint 64-bit, roughly time-sortable, collision-free IDs across thousands of machines with no central bottleneck — the sub-problem behind URL shorteners, tweet IDs, and every sharded primary key.

system-designidssnowflakedistributed-systems

Prompt

Generate unique IDs across many servers: never a duplicate, thousands per second per node, ideally roughly time-sortable, no single point of failure, and small enough (~64 bits) to fit a BIGINT primary key.

Why this one, and at what level

It looks trivial ("just use a counter") until you remove the single database — then it's a genuine distributed-systems problem. SDE-1: the UUID-vs-auto-increment trade-offs. SDE-2: derive Snowflake and its bit layout. SDE-3 / Staff: clock-skew handling, machine-id assignment under autoscaling, and k-sortability vs strict monotonicity. Shows up at Amazon, Twitter/X, Uber, Instagram, Stripe, usually as the follow-up to a URL shortener (see url-shortener) or feed design.

Requirements

Unique across all nodes forever; high throughput (thousands–millions/s); low latency (ideally generated locally, no network hop); roughly time-sortable (helps index locality and cursor pagination); compact (64-bit); highly available (the ID service can't be the thing that takes the site down).

The candidates

ApproachUniqueSortableThe catch
UUID v4 (random 128-bit)128-bit, random → terrible B-tree index locality; leaks nothing but sorts by nothing
DB auto-incrementsingle DB is a SPOF + write bottleneck; multi-master needs step/offset hacks
DB ticket + ranges~each node leases a block (e.g. 1,000 IDs), refills before exhaustion → few DB hits, but gaps and coordination
Redis INCRatomic and fast, but a network hop and Redis becomes a critical dependency
Snowflakegenerated locally, sortable, 64-bit — the standard answer; the only hard part is clocks

Snowflake — the bit layout

Pack three things into 64 bits so the ID is unique and time-ordered:

 0 | 41 bits: ms since custom epoch | 10 bits: machine id | 12 bits: sequence
sign        ~69 years                  1024 nodes            4096 ids / ms / node
  • The timestamp is the high bits, so numeric order ≈ time order (k-sortable).
  • 12-bit sequence ⇒ 4,096 IDs per millisecond per node = ~4M/s/node; across 1,024 nodes that's billions/s with no coordination on the hot path.
class Snowflake {
  private seq = 0;
  private lastMs = -1;
  constructor(private machineId: number, private epoch = 1700000000000) {}

  next(): bigint {
    let now = Date.now();
    if (now < this.lastMs) throw new Error("Clock moved backwards — refuse to issue");
    if (now === this.lastMs) {
      this.seq = (this.seq + 1) & 0xfff;            // 12-bit sequence
      if (this.seq === 0) now = this.waitNextMs(this.lastMs); // overflow → next ms
    } else {
      this.seq = 0;
    }
    this.lastMs = now;
    return (BigInt(now - this.epoch) << 22n)
         | (BigInt(this.machineId) << 12n)
         | BigInt(this.seq);
  }
  private waitNextMs(prev: number) { let t = Date.now(); while (t <= prev) t = Date.now(); return t; }
}

The genuinely hard part — clocks

The high bits are a wall-clock timestamp, so time going backwards breaks uniqueness:

  • Clock skew / NTP corrections: machines drift and NTP can step the clock back. If now < lastMs, you must not reissue IDs. Options: refuse (fail fast), wait until the clock catches up, or keep a logical clock that only moves forward. Naming this trade-off is the SDE-3 signal.
  • Machine-id assignment: each node needs a unique 10-bit id. Lease it from ZooKeeper/etcd, derive it from a Kubernetes StatefulSet ordinal, or hand it out at deploy time — and reclaim it when a node dies so autoscaling doesn't collide.
  • Sequence overflow: more than 4,096 IDs in one millisecond ⇒ spin until the next ms.

Variants & trade-offs

  • k-sortable, not strictly monotonic: two nodes in the same ms produce IDs that sort by machine id, not true issue order. Usually fine; say so if strict ordering matters.
  • ULID / KSUID (128-bit, lexicographically sortable strings) when you want a sortable string id instead of a 64-bit int.
  • Instagram's scheme: DB sharding with a per-shard sequence baked into a stored procedure — IDs encode the shard, which helps routing.

Design drills

Design drills: Distributed ID Generator0/3 done

Whiteboard each one out loud for 5–10 minutes before you reveal what a strong answer covers — the gap between your sketch and the checklist is your study list. Progress is saved on this device.

Core

Assign unique 10-bit machine ids to nodes that autoscale up and down all day.

Stretch

An NTP sync steps the clock back 50 ms. Keep IDs unique.

Core

IDs must not leak how many you've issued (no guessable sequence).