Designing a Unique ID Generator

A system design interview guide to handing out identifiers that are unique across every machine in a distributed system, without a central bottleneck, while staying small, numeric, and roughly ordered by time.

Almost every distributed system needs to label things: a new row, a message, an order, an uploaded photo. On a single database the answer is easy — an auto_increment column hands out 1, 2, 3, and the database guarantees no two rows collide. The moment you have many machines minting IDs at once, that guarantee disappears. You cannot ask a single counter for every ID without turning it into a bottleneck and a single point of failure, yet you still need every ID to be globally unique. This guide works through the requirements, compares the common approaches and where they break, and then explains the Snowflake design in detail, including the one subtle failure mode — clock skew — that every time-based scheme has to handle.

Contents

  1. Requirements
  2. Approaches and Their Drawbacks
  3. The Snowflake Approach
  4. Snowflake Fields and the Math
  5. Clock Skew and Rollback
  6. Summary

1. Requirements

Before choosing a design, pin down what an ID actually has to satisfy. In an interview these constraints are what justify ruling out simpler options, so state them explicitly.

RequirementWhy it matters
UniqueNo two IDs may ever collide, across all machines and data centers, for the life of the system. This is the non-negotiable property.
NumericIDs should be numeric values only. Numeric IDs index efficiently, compare cheaply, and avoid the size and parsing cost of strings.
Fits in 64 bitsAn ID must fit in a 64-bit integer. That keeps it compact in storage, in indexes, and on the wire, and it is a natural size for languages and databases to handle.
Sortable by timeIdeally, IDs generated later sort higher than IDs generated earlier. Time-ordered IDs make range scans, pagination, and "most recent" queries cheap.
High throughputThe system should be able to mint a large number of IDs per second — on the order of thousands per node per millisecond — without coordination on the hot path.
The "sortable by time" requirement is the one that quietly drives the whole design. If you did not care about ordering, a random 64-bit number would almost do. Wanting IDs to increase with time is what pushes you toward encoding a timestamp directly into the ID.

2. Approaches and Their Drawbacks

There are several well-known ways to generate IDs in a distributed setting. Each makes a different trade, and walking through why the first three fall short is the cleanest way to motivate the fourth.

ApproachHow it worksDrawbacks
Multi-master replicationUse several database servers, each with auto_increment, but have each one increment by k (the number of servers) instead of by 1, with a different starting offset per server. Server 1 yields 1, 4, 7, ...; server 2 yields 2, 5, 8, ...; and so on, so no two servers ever produce the same number.Hard to scale across multiple data centers, since the offsets must be coordinated globally. It does not scale well when servers are added or removed: changing k reshuffles every server's step, and IDs are not sortable by time across servers.
UUIDEach server independently generates a 128-bit Universally Unique Identifier. With enough random bits the chance of a collision is negligible, and no coordination between servers is needed at all.A UUID is 128 bits, but the requirement is 64 bits, so it does not fit. UUIDs are not time-ordered (a v4 UUID is essentially random), and they can be non-numeric — typically rendered as a hex string with hyphens.
Ticket serverRun a single, central database whose sole job is to hand out the next number from one auto_increment counter. Every service that needs an ID asks this one server.The ticket server is a single point of failure: if it goes down, no one can mint IDs anywhere. It is also a central bottleneck for throughput. Running multiple ticket servers to fix this reintroduces the multi-master coordination problem.
SnowflakeEach node generates IDs locally by packing a timestamp, the node's own identity, and a per-millisecond sequence counter into a single 64-bit number. No network call is needed to mint an ID.Few, for these requirements — which is why it is the chosen approach. It is distributed, time-sortable, and 64-bit. Its one real subtlety is dependence on the node clock, covered later.

The pattern across the first three is a tug-of-war between coordination and independence. Multi-master and the ticket server stay correct by coordinating, which limits scaling and creates bottlenecks. UUIDs avoid coordination entirely but pay for it in size and the loss of ordering. Snowflake threads the needle: it needs no coordination on the hot path, yet still produces compact, ordered, numeric IDs.

3. The Snowflake Approach

The key insight behind Snowflake (originally from Twitter) is to divide and conquer the 64 bits. Instead of treating an ID as one opaque counter, you partition the bits into sections, and let a different source of uniqueness fill in each section. As long as the combination of all sections is unique, the whole ID is unique — and crucially, each section can be filled in locally, with no call to a central server.

Twitter Snowflake 64-bit ID layout
The 64-bit Snowflake layout: 1 sign bit (always 0), 41 bits of timestamp, 5 bits of data center ID, 5 bits of machine ID, and a 12-bit sequence number. The high-order timestamp bits make the ID roughly sortable by time.

Reading the layout from the most significant bit to the least:

Uniqueness comes from the combination. Two IDs differ if they were made at different times (timestamp differs), or on different nodes (data center / machine ID differ), or on the same node in the same millisecond but at a different point in that millisecond (sequence differs). There is no way for two valid IDs to share all four fields.

# generating one ID on a node, no network call
function next_id():
  ts = current_millis()
  if ts == last_ts:
    sequence = (sequence + 1) AND 0xFFF   # 12-bit wrap (0..4095)
    if sequence == 0:
      ts = wait_until_next_millis(last_ts) # exhausted this ms
  else:
    sequence = 0
  last_ts = ts
  return ((ts - CUSTOM_EPOCH) << 22)        # 41 bits, shifted up
       OR (datacenter_id << 17)             # 5 bits
       OR (machine_id << 12)                # 5 bits
       OR sequence                          # 12 bits

4. Snowflake Fields and the Math

Each field's bit width sets a concrete limit. Working through the arithmetic shows why these particular sizes are a sensible default, and what each one buys you.

FieldBitsRange and meaning
Sign bit1Always 0. Reserved so the 64-bit value is always interpreted as a positive signed integer, which keeps comparisons and storage simple.
Timestamp41Milliseconds since a custom epoch chosen by the system, not the Unix epoch. 41 bits hold 241 milliseconds ≈ 69 years of range, so the generator can run for about 69 years from its epoch before the field overflows.
Data center ID525 = 32 data centers.
Machine ID525 = 32 machines per data center. Combined with the data center ID, that is 32 × 32 = 1024 nodes total.
Sequence number12212 = 4096 IDs per millisecond per node. Resets to 0 at the start of each new millisecond.

Two numbers are worth saying out loud in an interview. First, the custom epoch: by counting milliseconds from a recent date instead of 1970, you do not waste the 41-bit timestamp range on decades that already passed, so the ~69 years of headroom start from when the system was deployed. Second, the throughput ceiling: 4096 IDs per millisecond per node is just over four million IDs per second per node, and with up to 1024 nodes the system as a whole can mint on the order of four billion IDs per second — far beyond what most systems need.

Putting the fields together, the resulting IDs are roughly monotonic by time and globally unique without coordination. "Roughly" monotonic, not strictly: IDs generated on different nodes in the same millisecond are ordered by node ID rather than by the instant within the millisecond, so two IDs a fraction of a millisecond apart on different nodes may not sort in true wall-clock order. For pagination and range scans this is more than good enough.

If your throughput needs are lower but you have more nodes, you can re-budget the bits — for example, fewer sequence bits and more machine-ID bits. The 41/5/5/12 split is a default, not a law. The only fixed parts are the single sign bit and the total of 64.

5. Clock Skew and Rollback

Snowflake's elegance comes from trusting the local clock, and that trust is also its one weak point. The timestamp field assumes the clock only ever moves forward. In reality, a server's clock can jump backwards — most commonly when NTP (the Network Time Protocol) corrects a clock that had drifted ahead, or after a manual time change. This is clock skew, and if it is ignored it can break uniqueness.

The danger is concrete. Suppose a node mints IDs at millisecond T, then its clock is corrected backwards to T - 5. As it generates again, it will produce timestamps it has already used. Combined with the same node ID and a sequence counter that may also repeat, the node can emit an ID identical to one it produced five milliseconds earlier — a collision, the one thing the system must never do.

The mitigations form a layered defense:

# guarding against a backward clock
function next_id():
  ts = current_millis()
  if ts < last_ts:                          # clock moved backwards
    if (last_ts - ts) <= MAX_TOLERANCE:
      ts = wait_until(last_ts)              # small skew: block and recover
    else:
      raise ClockMovedBackwardError         # large skew: refuse to serve
  # ... normal sequence + pack as before ...
The takeaway: a time-based ID generator is only as trustworthy as the clock under it. The design does not assume perfect clocks — it assumes clocks are mostly forward-moving and handles the exceptions explicitly by never issuing an ID with a timestamp it has already passed.

6. Summary

Designing a unique ID generator is an exercise in getting global uniqueness without global coordination. Snowflake achieves it by partitioning 64 bits so each part can be filled in locally.

ConcernHow the design addresses it
What must an ID be?Unique, numeric, 64-bit, sortable by time, mintable at high throughput.
Why not multi-master auto_increment?Hard to scale across data centers and does not handle servers being added or removed cleanly.
Why not UUID?128 bits (not 64), not time-ordered, and often non-numeric.
Why not a ticket server?Single point of failure and a central throughput bottleneck.
Why Snowflake?Distributed, time-sortable, and fits in 64 bits, with no coordination on the hot path.
How are the 64 bits used?1 sign + 41 timestamp + 5 data center + 5 machine + 12 sequence.
What do the field sizes buy?~69 years of timestamps, up to 1024 nodes, and 4096 IDs per millisecond per node.
What is the main risk?Clock skew or rollback; mitigated by refusing to go backwards, waiting, NTP, and failing loudly on large jumps.
The recurring theme: split the problem so uniqueness comes from a combination of independent fields, encode time in the high bits to get ordering for free, and treat the local clock as the one thing that needs guarding. Everything else follows from those three choices.