Easy

Design a Unique ID Generator (Snowflake)

Design a service that hands out unique, roughly time-sorted 64-bit IDs at high throughput across many machines, in the style of Twitter Snowflake. The core challenge is generating IDs without coordination between nodes while keeping them sortable and collision-free.

1Requirements

Functional

  • Generate globally unique 64-bit numeric IDs with no duplicates across all machines.
  • IDs must be roughly sortable by creation time so newer IDs compare greater than older ones.
  • Support ID generation from many application servers or datacenters without a central coordinator on the hot path.
  • Expose a simple API or library call that returns an ID in a single round trip or in-process call.
  • Allow extracting the embedded timestamp from an ID for debugging and analytics.

Non-functional

  • Throughput of at least 10,000 IDs per second per machine, with headroom for bursts.
  • Latency under 1 millisecond per ID, ideally an in-process call with no network hop.
  • High availability: the generator must not become a single point of failure for every write in the system.
  • Correctness under clock skew: NTP adjustments or leap seconds must never produce duplicate IDs.
  • IDs should fit in a signed 64-bit integer so they work as primary keys in Postgres, MySQL, and Java longs.

2Back-of-envelope estimation

Timestamp bits41 bits
Machine capacity4,096 IDs/ms
Peak per machine4.1M IDs/sec
Worker ID space1,024 workers
Storage per ID8 bytes

3API design

GET /v1/id

Returns a single new 64-bit ID as a JSON string (stringified to avoid JavaScript number precision loss beyond 2^53).

GET /v1/ids?count=100

Batch endpoint returning up to 1,000 IDs in one call so clients amortize network overhead.

GET /v1/id/{id}/decode

Debug endpoint that unpacks an ID into its timestamp, datacenter ID, machine ID, and sequence number.

4High-level design

The Snowflake layout packs four fields into one 64-bit integer: 1 unused sign bit, 41 bits of milliseconds since a custom epoch, 5 bits of datacenter ID, 5 bits of machine ID, and 12 bits of per-millisecond sequence. Because the timestamp occupies the high bits, IDs sort by creation time, which keeps B-tree inserts append-friendly and lets you paginate by ID instead of a separate created_at column.

Each application server runs the generator as an in-process library. On startup it acquires a unique worker ID (datacenter + machine) from a small coordination store such as ZooKeeper, etcd, or even a database table with a unique constraint. After that, ID generation requires no network calls at all: the node reads its local clock, increments a sequence counter, and bit-shifts the fields together.

Within one millisecond, the 12-bit sequence counter distinguishes IDs. If a node exhausts 4,096 IDs in a single millisecond, it spins until the next millisecond tick. Across milliseconds the counter resets to zero. Across machines, uniqueness comes from the worker ID bits, so two machines can never collide even at the exact same timestamp and sequence.

Alternatives worth naming in an interview: UUIDv4 is coordination-free and simple but is 128 bits, random (terrible for B-tree locality), and not time-sortable; UUIDv7 fixes sortability but is still 16 bytes. A database ticket server (Flickr style) uses REPLACE INTO on an auto-increment table, which is simple but adds a network hop and a single point of failure, usually mitigated by two servers handing out odd and even IDs. Snowflake is the sweet spot when you need compact, sortable, high-throughput IDs.

5Data model

worker_registry

worker_id, datacenter_id, machine_id, hostname, leased_until, created_at

Stored in etcd or a small SQL table; each node leases a worker_id at boot and renews it.

id_layout (conceptual)

sign(1), timestamp_ms(41), datacenter_id(5), machine_id(10 combined), sequence(12)

Not a table; the bit layout of the 64-bit integer itself.

6Deep dives

Handling clock skew and backwards clocks

The generator trusts the local clock, so a backwards clock jump is the main correctness hazard. If NTP steps the clock back 50 ms, the node could re-issue timestamps it already used, and with a repeated sequence number that means duplicate IDs. The standard defense is to remember the last timestamp used: if the current clock reads earlier than that, either refuse to generate (throw and let the caller retry) or spin-wait until the clock catches up if the skew is small, say under 10 ms.

Operationally, you prevent large jumps by running NTP in slew mode (which speeds up or slows down the clock gradually instead of stepping it) and by refusing to start the generator if the clock looks wildly wrong compared to the lease store. Some implementations also reserve a few bits or a fallback sequence range to survive small regressions without blocking.

A subtler issue is worker ID reuse: if a node dies and another node takes its worker ID while the old process is still running (a zombie), both generate with the same worker bits. Leases with TTLs plus a startup wait of one lease period close this hole.

Why not just UUIDs or auto-increment

UUIDv4 needs zero coordination and never blocks, which is genuinely attractive. The costs: 16 bytes instead of 8, no time ordering, and random inserts that scatter writes across the entire primary key B-tree, causing page splits and cache misses at scale. UUIDv7 embeds a millisecond timestamp in the high bits and largely fixes locality, so it is the modern default when 128 bits are acceptable.

Database auto-increment is perfect on a single node but breaks under sharding: two shards will both hand out ID 1001. Workarounds include offset-and-stride (shard 1 issues 1, 3, 5 and shard 2 issues 2, 4, 6) or Flickr-style ticket servers, where a dedicated MySQL pair with auto_increment_increment=2 hands out blocks. Both add either operational rigidity or a network hop to every insert.

Snowflake trades a small amount of setup complexity (worker ID assignment, clock discipline) for coordination-free generation, compact sortable IDs, and per-node throughput that no ticket server can match.

Choosing the bit budget

The 41-5-5-12 split is a default, not a law. Every bit you move is a tradeoff along three axes: lifespan (timestamp bits), fleet size (worker bits), and burst rate (sequence bits). 41 timestamp bits from a 2020 epoch last until roughly 2089. If you only ever run 64 generators, you can shrink worker bits to 6 and give the extra bits to the sequence, doubling per-millisecond capacity.

Some systems use second-level rather than millisecond timestamps with a much larger sequence, which tolerates coarse clocks better but weakens sort granularity. Instagram's variant uses 41 bits of time, 13 bits of logical shard ID, and 10 bits of a per-shard sequence generated inside Postgres itself, showing the same idea can live inside the database as a PL/pgSQL function.

Whatever split you choose, publish it as a constant and never change it in place: reinterpreting the bits of already-issued IDs silently corrupts ordering and decoding.

7Rapid implementation: build the MVP

Theory is table stakes. Here is how you would stand up a working version fast, on a budget, with the core algorithm in real code.

Stack

Node.js (TypeScript) library + a 3-line etcd or Postgres worker-ID lease, embedded in any API server; zero extra infrastructure.

  1. 01Pick a custom epoch (e.g. 2024-01-01T00:00:00Z) and define the bit layout as constants: 41 timestamp, 10 worker, 12 sequence.
  2. 02Write the generator class with BigInt bit-shifting and a lastTimestamp guard that throws on clock regression.
  3. 03Create a worker_leases table in Postgres with a UNIQUE(worker_id) constraint; on boot, INSERT the first free ID in 0..1023 with a leased_until timestamp.
  4. 04Add a renewal loop that extends the lease every 30 seconds and kills the process if renewal fails twice.
  5. 05Wrap the generator in a GET /v1/id route that returns the ID as a string, plus a /decode route for debugging.
  6. 06Load test with autocannon at 50k req/sec and verify zero duplicates by inserting all IDs into a UNIQUE column.
  7. 07Add a unit test that mocks Date.now going backwards and asserts the generator throws instead of duplicating.

Snowflake bit-packing generator

typescript
const EPOCH = 1704067200000n; // 2024-01-01 UTC
const WORKER_BITS = 10n;
const SEQ_BITS = 12n;
const MAX_SEQ = (1n << SEQ_BITS) - 1n; // 4095

export class Snowflake {
  private lastTs = -1n;
  private seq = 0n;
  constructor(private workerId: bigint) {
    if (workerId < 0n || workerId > 1023n) throw new Error("worker id out of range");
  }
  next(): bigint {
    let ts = BigInt(Date.now());
    if (ts < this.lastTs) throw new Error("clock moved backwards");
    if (ts === this.lastTs) {
      this.seq = (this.seq + 1n) & MAX_SEQ;
      if (this.seq === 0n) {
        while (ts <= this.lastTs) ts = BigInt(Date.now()); // spin to next ms
      }
    } else {
      this.seq = 0n;
    }
    this.lastTs = ts;
    return ((ts - EPOCH) << (WORKER_BITS + SEQ_BITS)) | (this.workerId << SEQ_BITS) | this.seq;
  }
}

Decode an ID back into its fields

typescript
const EPOCH_MS = 1704067200000; // must match the generator's epoch

export function decode(id: bigint) {
  const seq = id & 0xfffn;               // low 12 bits
  const workerId = (id >> 12n) & 0x3ffn; // next 10 bits
  const tsOffset = id >> 22n;            // high 41 bits
  const createdAtMs = Number(tsOffset) + EPOCH_MS;
  return {
    createdAt: new Date(createdAtMs),
    createdAtMs,
    workerId: Number(workerId),
    sequence: Number(seq),
  };
}

Worker ID lease in Postgres

sql
CREATE TABLE worker_leases (
  worker_id INT PRIMARY KEY CHECK (worker_id BETWEEN 0 AND 1023),
  hostname TEXT NOT NULL,
  leased_until TIMESTAMPTZ NOT NULL
);

-- Claim the first free or expired worker id atomically
INSERT INTO worker_leases (worker_id, hostname, leased_until)
SELECT gs.id, 'api-7', now() + interval '60 seconds'
FROM generate_series(0, 1023) AS gs(id)
WHERE NOT EXISTS (
  SELECT 1 FROM worker_leases w
  WHERE w.worker_id = gs.id AND w.leased_until > now()
)
ORDER BY gs.id
LIMIT 1
ON CONFLICT (worker_id) DO UPDATE
  SET hostname = EXCLUDED.hostname, leased_until = EXCLUDED.leased_until
  WHERE worker_leases.leased_until <= now()
RETURNING worker_id;

Bottlenecks & failure modes

  • Backwards clock jumps can cause duplicates; must track last timestamp and block or error on regression.
  • Worker ID assignment is the one coordination point; a misconfigured duplicate worker ID silently produces collisions.
  • The 4,096 per-millisecond sequence cap makes a single node spin under extreme bursts; batch requests or add nodes.
  • JavaScript clients corrupt IDs above 2^53 if APIs return them as JSON numbers; always serialize as strings.
  • A centralized ID service (instead of an in-process library) puts a network hop and a failure domain on every write path.

Key takeaways

  • Uniqueness comes from partitioning the ID space (worker bits), not from coordination on the hot path.
  • Time-sortable IDs double as creation timestamps and keep B-tree inserts sequential.
  • Clock skew is the central failure mode; the last-timestamp guard is non-negotiable.
  • Know the alternatives cold: UUIDv4 (simple, fat, unsorted), UUIDv7 (sorted, still 16 bytes), ticket servers (simple, centralized).
  • Bit budgets are tunable: lifespan vs fleet size vs burst throughput.

Brush up on the underlying topics