Day 8 B-Building blocks 2026-10-02 ← All lessons

Unique ID generation in distributed systems: snowflake

A single database hands out IDs from one counter, and one counter cannot feed a fleet. This lesson builds the snowflake: 64 bits split so every machine mints sortable IDs without asking anyone.

10000Sustained throughput required
4096IDs one machine mints per millisecond
1024Datacenters times machines per datacenter
69Runway of the 41-bit timestamp

Key points

Outcomes

01Name the requirement each rejected option breaks: multi-master, UUID, and ticket server.
02Read a snowflake ID aloud: timestamp orders it, datacenter plus machine locate it, sequence splits the millisecond.
03Derive the three capacities from the bit widths: 4,096 per ms, 1,024 nodes, and the 69 year horizon.
01

One counter cannot feed a fleet


A traditional primary key uses auto_increment on one database: each insert takes the next integer. That works while one server holds all writes. Split writes across databases and the single counter becomes a bottleneck, cross-database ordering breaks, and adding or removing a server reshuffles the scheme. The interview sets five requirements up front: IDs must be unique, numeric only, fit in 64 bits, ordered by date, and flow at over 10,000 per second. Every candidate below is judged against exactly these five.

Interview tipInterview line: recite the five requirements before proposing anything. Graders check whether you design against constraints or against vibes.
02

Four candidates, three rejections


The chapter tries three designs before snowflake. Multi-master replication staggers auto_increment by server count, so two servers mint odd and even IDs. It fails three ways: painful across data centers, IDs do not rise with time across servers, and membership changes hurt. UUIDs need no coordination at all, but they are 128 bits, unsorted, and can carry non-numeric characters. Ticket servers centralize auto_increment in one allocator, which is simple for small scale yet is a single point of failure, and fixing that with more ticket servers reintroduces synchronization.

ApproachHow it worksRequirement it breaks
Multi-master auto_incrementEach of k servers steps its counter by kTime ordering across servers; scaling membership
UUID, 128-bitEach server mints randomly, no coordination64-bit fit; time ordering; numeric only
Ticket serverOne central allocator hands out IDsAvailability: single point of failure
SnowflakeEach machine composes timestamp plus node plus sequenceNone: needs clock discipline instead
Each candidate against the five requirements
Option A

Snowflake: sortable 64-bit

  • 64 bits, numeric, sortable by time
  • No coordination per ID after startup config
  • 1,024 nodes mint at full speed in parallel
Option B

UUID: coordination-free 128-bit

  • 128 bits, unsorted, sometimes non-numeric
  • Zero config: mint anywhere with no setup
  • 1 in 2 to the 122 per pair: collisions need no planning
03

Divide the 64 bits


Snowflake stops generating an ID and starts composing one. The 64 bits are five fields: 1 sign bit fixed at 0, 41 bits of milliseconds since a custom epoch, 5 bits of datacenter ID, 5 bits of machine ID, and a 12-bit sequence number. Because the timestamp sits in the most significant bits, IDs sort by creation time with plain integer comparison. The custom epoch matters: Twitter used 1288834974657, which is Nov 04 2010, so the 41-bit counter starts near birth instead of wasting its range on the decades before the system existed.

  1. Sign, 1 bit, always 0: reserved, keeps values non-negative.

  2. Timestamp, 41 bits: ms since custom epoch. Max 2^41 minus 1 ms, about 69 years of runway.

  3. Datacenter ID, 5 bits: 32 datacenters.

  4. Machine ID, 5 bits: 32 machines per datacenter, 1,024 minting nodes total.

  5. Sequence, 12 bits: 4,096 IDs per ms per machine. Zero except when a millisecond carries more than one ID.

Required throughput
10K/s
One machine at 4,096 per ms
4.1M/s
Full fleet of 1,024 machines
4.2B/s
πŸ“ Log scale β€” each step right is ~10Γ—. Bar lengths show order of magnitude, not raw proportion.
Headroom on a log scale: one machine alone clears the 10,000 per second bar 400 times over.
04

Mint one ID, live


Datacenter and machine IDs are picked at startup and then frozen: changing them mid-flight risks two nodes minting as one. Only the timestamp and sequence move at request time. When a request arrives, the generator reads the wall clock in milliseconds. If the millisecond matches the previous ID, it bumps the sequence. If the clock has advanced, it resets the sequence to zero. Then it packs the five fields into one integer and returns it. The two timing paths below are the whole state machine.

How to read: Follow the dots: the request enters the generator, takes the upper path when the millisecond repeats or the lower path when the clock advances, and leaves as one packed integer.

App serverID generatorSame ms: seq + 1New ms: seq = 064-bit ID
  1. App asks its local generator for an ID.

  2. Generator reads the wall clock in milliseconds.

  3. Same ms as the last ID: increment the sequence.

  4. New ms: reset the sequence to zero.

  5. Pack sign, timestamp, datacenter, machine, sequence.

  6. Return one sortable 64-bit integer.

One request, two timing paths: same millisecond bumps the sequence, a new millisecond resets it.
  1. Read current time in ms. If it is behind the last timestamp, wait: never mint on a stale clock.

  2. If time equals last timestamp, sequence plus 1. If sequence overflows 4,096, wait for the next ms.

  3. If time is ahead, take the new timestamp and reset sequence to 0.

  4. Shift and OR the fields: time at bit 22, datacenter at 17, machine at 12, sequence at 0.

  5. Return the integer. Evening IDs exceed morning IDs by construction.

05

Where the model lies


Four weaknesses, each with a price tag. Clock synchronization is the big one: the design assumes every generator shares the same clock, which breaks across cores and machines, so NTP plus monitoring is part of the system, not an afterthought. A backward clock must stall ID generation until time catches up, or new rows sort before old ones. Bursts past 4,096 IDs in one ms on one machine also stall into the next millisecond, trading latency for uniqueness. Node IDs are operationally load-bearing: a duplicated datacenter or machine ID mints true collisions, so allocation needs a registry and a review, not a config edit. And the 69-year timestamp eventually overflows, which a fresh epoch or a migration must handle.

GotchaGotcha: a machine whose clock runs backward mints IDs that sort before rows created earlier. Treat clock skew as a correctness bug, not a monitoring nicety.

In the field, Twitter mints tweet IDs with snowflake because timelines and cursors sort directly on the ID. Flickr chose ticket servers instead because photo uploads fit a central allocator at their scale. Same problem, different price accepted: coordination-free minting paid for with clock discipline, or simplicity paid for with a single point of failure. An ID generator is mission-critical either way, so high availability is a requirement, not a stretch goal.

06

Code it: a snowflake in 30 lines


The lines that matter are the clock guard, the sequence overflow wait, and the bit shifts. Everything else is argument plumbing.

python
EPOCH = 1288834974657  # Nov 04 2010, custom epoch saves range
SEQ_BITS, M_BITS, DC_BITS = 12, 5, 5
MAX_SEQ = (1 << SEQ_BITS) - 1

class Snowflake:
    def __init__(self, datacenter_id, machine_id):
        assert 0 <= datacenter_id < 32 and 0 <= machine_id < 32
        self.dc, self.mc = datacenter_id, machine_id
        self.last_ms, self.seq = -1, 0

    def _now_ms(self):
        import time
        return int(time.time() * 1000) - EPOCH

    def next_id(self):
        ms = self._now_ms()
        if ms < self.last_ms:
            raise ClockBackwardError(ms, self.last_ms)  # never mint stale time
        if ms == self.last_ms:
            self.seq += 1
            if self.seq > MAX_SEQ:
                ms = self._wait_next_ms(self.last_ms)  # burst: trade latency
                self.seq = 0
        else:
            self.seq = 0  # fresh millisecond, fresh sequence
        self.last_ms = ms
        return (ms << 22) | (self.dc << 17) | (self.mc << 12) | self.seq

    def _wait_next_ms(self, last):
        ms = self._now_ms()
        while ms <= last:
            ms = self._now_ms()
        return ms

class ClockBackwardError(Exception):
    pass

g = Snowflake(3, 9)
ids = [g.next_id() for _ in range(5)]
print(ids)
print('sorted by time:', ids == sorted(ids))
print('all unique:', len(set(ids)) == len(ids))
Interview tipInterview line: when asked for extra depth, offer clock sync, section length tuning, and high availability, in that order.
Q&A

Check yourself


Q1One generator's clock jumps 5 seconds backward. What is the safe behavior?
  • Keep minting with the last timestamp
  • Refuse or wait until the clock catches up
  • Reset the machine ID to zero
βœ“ Refuse or wait until the clock catches up β€” Waiting keeps every ID time ordered; minting on a stale clock would sort new rows before old ones.
Q2A flash sale needs 8,000 IDs in one millisecond from one machine. What happens?
  • Mint all 8,000, the sequence wraps safely
  • Stall into the next millisecond and take the latency hit
  • Steal 2 bits from the timestamp, it has spare room
βœ“ Stall into the next millisecond and take the latency hit β€” 12 bits hold 4,096 values, so 8,000 requests in one ms overflow the field and must spill into the next ms.
Q3Two datacenters both boot with datacenter 3 and machine 9. What breaks first?
  • IDs stay unique because sequences differ
  • Two machines can mint the identical ID
  • Timestamps diverge and ordering breaks
βœ“ Two machines can mint the identical ID β€” Same timestamp plus same node IDs plus same sequence equals the same 64 bits on both machines.
Sources: System Design Interview Vol 1: Ch. 7, Design a unique ID generator (pp. 110-118)