← System Design
Building Blocks

Unique ID Generation — UUIDs, Ticket Servers & Snowflake

Once data lives on many machines, a database's auto-increment column can no longer hand out IDs: each shard would produce its own 1, 2, 3. Distributed systems need IDs that are unique without coordination on every insert, fit in an index nicely, and often sort roughly by time. This page compares the standard options and when to use each.

In simple words

Like a railway PNR: the number itself encodes when and where it was issued, so every counter in the country can issue tickets without calling a central office, and no two tickets ever get the same number.

Snowflake: a 64-bit ID built locally41 bitstimestamp (ms)10 bitsworker id12 bitssequence64-bit IDtime-sortable
Time first (sortable), then machine (unique across workers), then a per-millisecond counter.

Deep Dive: The Options

ApproachSizeSortable by timeCoordinationNotes
DB auto-increment64-bityessingle DBsimple; bottleneck and SPOF at scale
Multi-master step (odd/even)64-bitroughlyfixed N serversserver k issues k, k+N, k+2N; hard to add servers
Ticket server / ID blocks64-bitroughlyone fetch per blockapps lease ranges of 1,000 ids; fast; central dependency
UUID v4128-bitnononerandom; poor B-tree locality; long
UUID v7 / ULID128-bityesnonetimestamp prefix + randomness; index-friendly
Snowflake64-bityesworker id assignmenttime + machine + sequence; very fast

Deep Dive: Snowflake

64-bit id (Twitter Snowflake layout)
| 1 sign | 41 bits: ms since custom epoch | 10 bits: worker id | 12 bits: sequence |
            ~69 years                        1,024 workers        4,096 ids/ms/worker

def next_id():
    now = current_millis()
    if now == last_ms:
        seq = (seq + 1) & 0xFFF
        if seq == 0:                 # 4,096 used this ms -> wait for next ms
            now = wait_until_next_ms(last_ms)
    else:
        seq = 0
    if now < last_ms: raise ClockMovedBackwards   # or wait / refuse
    last_ms = now
    return ((now - EPOCH) << 22) | (worker_id << 12) | seq

IDs from one worker are strictly increasing; across workers they are roughly time-ordered (k-sorted). The worker id must be unique: assign it from configuration, or lease it from ZooKeeper/etcd at startup.

Key Pointers

How It Works

  1. Decide the requirements: size (64 vs 128 bits), ordering, throughput, exposure to users.
  2. For Snowflake, assign each generator a unique worker id at startup.
  3. Generate IDs locally from time + worker id + per-millisecond sequence; no network call per ID.
  4. Monitor clock drift (NTP) and refuse to generate if the clock goes backwards.

Follow-up Questions

Why not just use UUID v4 everywhere?
It works, but 128 random bits make B-tree inserts land on random pages (page splits, poor cache hit rates) and IDs don't sort by time. Time-prefixed IDs (v7, ULID, Snowflake) keep inserts append-mostly.
What happens if two machines get the same worker id?
They can generate duplicate IDs in the same millisecond. Lease worker ids with a TTL from a coordination service so a crashed node's id isn't reused while it could still be running.
Could you use Redis INCR?
Yes for moderate scale (it's atomic and fast), but it becomes a central dependency, and durability depends on Redis persistence settings; block leasing reduces the round trips.

Trade-offs

Coordination-free schemes (UUID) are simplest but bigger and less index-friendly. Snowflake is compact and ordered but needs worker id management and sane clocks. Central counters are simple and dense but add a dependency. Pick by size, ordering and operational tolerance.

Related