The standard answer to "design a unique ID generator" for a distributed system is a Snowflake-style design: a 64-bit integer built from a millisecond timestamp, a machine (worker) ID and a per-millisecond sequence number. Every server mints IDs locally with no per-ID coordination, the IDs sort roughly by creation time, and the main risk you have to handle is the system clock moving backward. Before you commit, show why UUIDs, database auto-increment and a ticket server fall short.
The specifics below come from Twitter's Snowflake README and source code, Flickr's ticket server post, Discord's API docs, the Sonyflake README and RFC 9562, the current UUID standard.
1. Clarify the requirements first
"Generate unique IDs" is underspecified on purpose. Ask before you draw anything:
- Scope of uniqueness. Unique across the whole system, or only within one table or shard?
- Format and size. Numeric only? Must it fit in 64 bits (a
BIGINTcolumn, a Javalong)? - Ordering. Do IDs need to sort by creation time? Strictly, or is "roughly" good enough?
- Throughput and latency. Peak IDs per second? Can generation be a network call, or must it be in-process?
- Availability. Is a single point of failure acceptable? Usually not, because every write needs an ID.
- Exposure and geography. Will IDs appear in public URLs, where sequential values are easy to enumerate? One datacenter or several?
When Twitter built Snowflake in 2010, it was moving storage from MySQL to Cassandra, which has no built-in sequential ID facility. The requirements in its README are a good model answer to write on the board:
- At least 10,000 IDs per second per process, with a response time of about 2 ms plus network latency.
- Uncoordinated: machines generate IDs without talking to each other.
- Roughly time-ordered (k-sorted within about one second, with tens of milliseconds as the goal) and directly sortable without loading the objects.
- Compact: 64 bits, not 128.
- At least as available as the storage it feeds.
2. The four options and their trade-offs
UUID. A UUID is 128 bits. Version 4 is 122 random bits, so any machine can generate one with no coordination and collisions are practically impossible. The costs: it is twice the size of a 64-bit key, and random values scatter inserts across a B-tree index. Flickr rejected GUIDs for exactly that reason: they are large and index poorly in MySQL. RFC 9562 (May 2024) added UUIDv7, which puts a 48-bit Unix millisecond timestamp in the high bits so values sort by time and index more locally. It is a strong answer when 128 bits is allowed; it does not meet a 64-bit requirement.
Database auto-increment. One database handing out AUTO_INCREMENT values is simple and strictly ordered, but it is a single point of failure and a write bottleneck. The multi-master variant gives N databases the same step: with two servers, one issues odd numbers and the other even. That removes the single point of failure but breaks ordering across servers and makes adding a server painful, because every step and offset has to change.
Ticket server. Flickr's 2010 design is a dedicated MySQL database with a one-row table. An app server runs REPLACE INTO Tickets64 (stub) VALUES ('a'); then SELECT LAST_INSERT_ID(); to get the next 64-bit value. For availability Flickr ran two ticket servers, one issuing odd and one even values (auto-increment-increment = 2, offsets 1 and 2), with clients round-robining between them; the two drift apart, so IDs are not strictly sequential. A common refinement is range allocation: an app server reserves a block of IDs at a time, cutting network calls but losing cross-server ordering and wasting the rest of a block on a crash.
Snowflake. Each worker combines its clock, its own worker ID and a counter. No network call per ID, 64-bit numeric, roughly time-sortable. The costs: it depends on the clock, each worker needs a unique ID, ordering across machines is only approximate, and anyone holding an ID can read its creation time.
| Approach | Size | Sorts by time? | Coordination | Main weakness |
|---|---|---|---|---|
| UUIDv4 | 128 bits | No | None | Large; random index inserts |
| UUIDv7 | 128 bits | Yes (ms) | None | Fails a 64-bit requirement |
| Single DB auto-increment | 64 bits | Yes, strictly | Every ID hits one database | Single point of failure, throughput ceiling |
| Multi-master with step | 64 bits | Not across servers | Fixed step and offset per server | Hard to add servers |
| Ticket server | 64 bits | Roughly | Network call per ID or per block | Central dependency, extra hop |
| Snowflake | 64 bits | Roughly (k-sorted) | Worker ID assignment only | Depends on the clock |
If the requirements are 64-bit, numeric, sortable and highly available, Snowflake is the only row that satisfies all of them. Say that out loud before you move on.
3. The Snowflake bit layout
The original layout uses 63 of the 64 bits. The top bit stays 0 so the value is positive in signed 64-bit types.
1 bit 41 bits 10 bits 12 bits
+------+--------------------------------+-------------+-------------+
| 0 | ms since custom epoch | worker ID | sequence |
+------+--------------------------------+-------------+-------------+
bit 63 bits 62-22 bits 21-12 bits 11-0
| Field | Bits | What it buys you |
|---|---|---|
| Sign | 1 | Always 0, so the ID is never negative in a long or BIGINT |
| Timestamp | 41 | 241 ms is about 69 years from your chosen epoch |
| Worker ID | 10 | 1,024 distinct generators |
| Sequence | 12 | 4,096 IDs per millisecond per worker |
Do the math out loud. 4,096 IDs per millisecond is a ceiling of about 4.1 million IDs per second per worker, and 1,024 workers is roughly 4.2 billion per second in total, far above a 10,000-per-second requirement. The epoch matters too: counting from 1970 would already have used almost 57 of the 69 years, and the IDs would overflow in 2039. Pick a custom epoch close to launch; Twitter's own code counts from November 2010, Discord's from the start of 2015.
The generator itself is a few lines. It must run under a lock (or on a single thread) per worker ID:
EPOCH = 1735689600000 # example: 2025-01-01 UTC in ms
last_ts = -1
seq = 0
def next_id():
global last_ts, seq
ts = now_ms()
if ts < last_ts:
ts = handle_clock_backward(last_ts) # see section 5
if ts == last_ts:
seq = (seq + 1) & 4095 # 12-bit wrap
if seq == 0: # 4,096 used this ms
ts = wait_until_after(last_ts)
else:
seq = 0
last_ts = ts
return ((ts - EPOCH) << 22) | (worker_id << 12) | seq
Because the timestamp sits in the high bits, (id >> 22) + EPOCH recovers the creation time from any ID. That helps with debugging and time-range queries, and it is also an information leak worth mentioning.
4. Assigning worker IDs
A correctly implemented Snowflake generator can produce a duplicate in only two ways: two live processes share a worker ID, or one worker's clock goes backward. Worker ID assignment handles the first. Common choices:
- Static configuration. Each host gets an ID in its config file. Simple, but a copy-pasted config silently creates duplicates.
- Lease from a coordination service. On startup, a worker claims a free ID from something like ZooKeeper or etcd and holds it with a lease or ephemeral node. Coordination happens once at boot, not once per ID.
- Derive it from the host. Sony's open-source Sonyflake uses the lower 16 bits of the machine's private IP address by default, and reserves 16 bits for the machine ID to match.
Whichever you pick, say how you prevent a restarted or cloned process from reusing an ID that is still live.
5. Handling clock skew
Clocks can move backward, for example after an NTP step correction or a manual change. Skew between machines only hurts ordering, because different worker IDs keep the values distinct. A single worker's clock going backward can hurt uniqueness, because it could reissue a timestamp-and-sequence pair it has already used.
- Refuse to generate until time catches up. This is what Twitter's Snowflake does: if the clock goes back, it rejects ID requests until the current time passes the timestamp of the last ID it generated.
- Size the response to the jump. For a gap of a few milliseconds, waiting it out costs callers almost nothing. For a large jump, fail fast, alert, and take the node out of rotation rather than stalling requests.
- Configure NTP to avoid stepping backward. The Snowflake README recommends running NTP in a mode that never moves the clock back, so small corrections are applied gradually.
- Survive restarts. A fresh process does not know its last timestamp. Persist a high-water mark periodically, or wait a safety margin on startup, so a reboot after a clock change does not repeat IDs.
- Or run on logical time. Keep issuing from the last timestamp, running slightly ahead of the wall clock until real time catches up. This trades accuracy of the embedded time for availability.
The one-line summary: Snowflake buys zero per-ID coordination at the price of depending on clocks. Raise clock skew before the interviewer does.
6. Going multi-datacenter
The usual move is to split the 10 machine bits into a datacenter field and a worker field. Twitter's own open-source IdWorker does exactly this: 5 datacenter bits and 5 worker bits, for 32 datacenters with 32 workers each. Discord's documented layout splits the same 10 bits into a 5-bit internal worker ID and a 5-bit internal process ID. What that buys you:
- No cross-region traffic per ID. The datacenter bits make values from different regions distinct by construction.
- Independent failure. If one region goes down, the others keep generating with no shared state to recover.
- Ordering stays approximate. Two IDs created a few milliseconds apart in different regions can sort in the "wrong" order. That is acceptable for feeds and logs but not for anything that needs a total order.
More generators means taking bits from somewhere: fewer sequence bits (lower per-worker rate) or fewer timestamp bits (shorter lifetime).
7. Real variations worth knowing
| Scheme | Size | Time field | Node field | Counter / random |
|---|---|---|---|---|
| Twitter Snowflake | 64 bits | 41 bits, ms | 10 bits | 12-bit sequence |
| Discord snowflake | 64 bits | 42 bits, ms since the start of 2015 | 5-bit worker + 5-bit process | 12-bit increment |
| Sonyflake | 63 bits used | 39 bits, 10 ms units (about 174 years) | 16 bits | 8-bit sequence |
| ULID | 128 bits | 48 bits, ms | None | 80 random bits |
| UUIDv7 | 128 bits | 48 bits, ms | None | Random, minus version and variant bits |
Two details make good follow-up material. Sonyflake gives up per-machine throughput (28 IDs per 10 ms per instance) for a longer lifetime and more machines; those are its defaults, and its v2 lets you change the bit widths and time unit. Discord returns its IDs as strings in its HTTP API to prevent integer overflow in some languages; JavaScript numbers are only exact up to 253 − 1, so a 64-bit ID sent as a JSON number can be silently rounded.
8. How to present it, and the follow-ups to expect
- Agree on requirements: 64-bit, numeric, sortable, highly available.
- Rule out the alternatives in a sentence each.
- Draw the bit layout; do the capacity and lifetime math aloud.
- Walk through
next_id(), including sequence overflow. - Raise clock skew and worker ID assignment unprompted.
- Close with the multi-datacenter split and trade-offs.
Likely follow-ups, with short answers:
- "What if you need more than 1,024 machines?" Rebalance the bit budget by taking bits from the sequence or the timestamp, or move to a wider 128-bit format.
- "Can someone guess the next ID?" Largely, yes. For public URLs, add a separate random token or encode the ID, as in a URL shortener that Base62-encodes a 64-bit ID.
- "How would you test it?" Generate millions of IDs across many threads and assert there are no duplicates, then inject a clock that steps backward and confirm the generator waits or refuses.
- "Can IDs be strictly increasing across the whole system?" Only with a single sequencer or consensus on every ID, which brings back the bottleneck you designed away.
- "Where does this sit in a larger design?" It shows up inside other questions, such as tweet IDs in Design Twitter and record keys in a distributed key-value store.
Knowing the design and explaining it under time pressure are different skills, so rehearse it aloud. A free AI mock interview in CoPilot Interview can include system design questions in the mix, reads each one aloud, and scores your spoken answer with what worked and what to improve. For the surrounding concepts, review the 14 core system design interview concepts.
Rehearse this design out loud
Paste a job description and your résumé, add system design to the question mix, and get a score on every answer plus an interview report with likely next-round questions. The free plan includes 15 minutes a day with no credit card.
Try a free AI mock interviewFAQ
What is a Snowflake ID?
It is a 64-bit ID format Twitter released in 2010: a 41-bit millisecond timestamp, a 10-bit machine ID and a 12-bit sequence number, with the top bit left at zero. Because the timestamp sits in the high bits, IDs sort roughly by creation time without machines coordinating with each other.
Why not just use a UUID?
A UUID is 128 bits, twice the size of a 64-bit key, and random UUIDv4 values scatter inserts across a B-tree index. If 128 bits is allowed, a time-ordered UUIDv7 from RFC 9562 is a reasonable choice; if the requirement is a 64-bit numeric ID, use a Snowflake-style design.
How many IDs per second can a Snowflake generator produce?
The 12-bit sequence allows 4,096 IDs per millisecond per worker, a ceiling of about 4.1 million per second per worker. Twitter's stated requirement was at least 10,000 per second per process, so the layout leaves a lot of headroom.
What happens if the clock goes backward?
Twitter's Snowflake refuses to generate IDs until the clock passes the timestamp of the last ID it issued, and its README recommends running NTP in a mode that does not move the clock backward. In an interview, also describe what you do for a large jump: alert and take the node out of rotation.
Are Snowflake IDs strictly ordered?
No. IDs from one worker always increase, but IDs from different machines are only roughly ordered, limited by clock differences between them. Twitter's target was ordering within about one second, aiming for tens of milliseconds.