Design a Unique ID Generator: Snowflake, UUIDs, and Distributed Clocks
How to design a distributed unique ID generator — Snowflake's 64-bit layout, UUID vs sequence trade-offs, worker-ID leasing, and clock-skew handling.
This article has an interactive companion
Don't just read it — step through it interactively, one state change at a time.
Every row in a distributed database needs a primary key, and the moment more than one machine mints those keys, "just use auto-increment" stops working — two servers can hand out 1001 at the same instant, corrupting a join, a payment, or a user account.
The unique-ID problem asks a deceptively narrow question: how do you generate identifiers that are globally unique, roughly time-ordered, and cheap to produce at millions per second, across data centers, with no central coordinator in the hot path? Get it right and every downstream index, sort, and shard key benefits; get it wrong and you either bottleneck on a single sequence server or scatter random 128-bit values that shred your B-tree indexes.
The reference answer is Twitter Snowflake: a 64-bit scheme that packs a timestamp, a machine ID, and a per-millisecond counter into one integer. Understanding why those three fields exist — and what breaks when clocks lie — is the whole interview.
Why not just use a database sequence or a UUID?
Start with the two obvious answers and watch them fail under load.
A database auto-increment sequence gives perfectly ordered, compact integers, but every ID needs a round trip to one authoritative row. At 50,000 writes/second that row becomes a contention hotspot, and it can't span regions without turning into a distributed-consensus problem — you've traded uniqueness for a global write bottleneck.
A UUIDv4 goes to the other extreme: 128 random bits, generated locally with zero coordination and collisions statistically impossible — but random values have no order. As a primary key, each insert lands at a random spot in the index, forcing page splits and destroying the cache locality a B-tree relies on; write throughput on a large table can drop by half, and the 128-bit width doubles index size versus a 64-bit integer.
Snowflake threads the needle: local generation like a UUID, but time-ordered and half the width, so it keeps index locality and fits in a standard BIGINT column.
What is the 64-bit layout of a Twitter Snowflake ID?
A Snowflake ID is one signed 64-bit integer, carved into four fields from the high bits down:
bit 63 | bits 62-22 (41 bits) | bits 21-12 (10 bits) | bits 11-0 (12 bits)
sign | timestamp | worker | sequence
1 bit ms since epoch 1024 nodes 4096 / ms- 1 sign bit, always
0, so the ID stays positive and sorts as an unsigned value would. - 41 bits of timestamp — milliseconds since a custom epoch (not the Unix epoch). 2⁴¹ ms is about 69 years of range, which is why you pick a recent epoch to buy the full lifetime.
- 10 bits of worker ID — 2¹⁰ = 1024 distinct nodes. Often split into 5 bits of data-center ID and 5 bits of machine ID.
- 12 bits of sequence — a per-millisecond counter, 2¹² = 4096 values, that disambiguates IDs minted inside the same millisecond on the same node.
Putting the timestamp in the high bits is the load-bearing decision. It makes IDs monotonically increasing over time, so a numeric sort is roughly a chronological sort, and new IDs cluster at the right edge of the index instead of scattering.
Composing one ID, bit by bit
Numbers make it concrete. Say the custom epoch is 1704067200000 (Jan 1 2024), a request arrives at wall-clock 1704067200123, on worker 42, and it is the 7th ID minted this millisecond (sequence 6, zero-indexed).
| Field | Value | Bits | Shift left by |
|---|---|---|---|
| timestamp | 1704067200123 − 1704067200000 = 123 | 41 | 22 |
| worker | 42 | 10 | 12 |
| sequence | 6 | 12 | 0 |
The generator assembles them with shifts and OR:
const id =
(BigInt(timestamp - EPOCH) << 22n) |
(BigInt(workerId) << 12n) |
BigInt(sequence);
// (123n << 22n) | (42n << 12n) | 6n => 515899392n | 172032n | 6n => 516071430nThe next request in the same millisecond bumps sequence to 7, producing 516071431n — strictly larger, so ordering holds within the node. When the clock ticks over, the timestamp field grows, sequence resets to 0, and the new ID dwarfs every ID from the prior millisecond regardless of worker.
Capacity: what 4,096 IDs per millisecond actually buys
The sequence field is 12 bits, so a single node can mint at most 4,096 IDs per millisecond, which is 4,096 × 1000 = ~4.1 million IDs per second per node. With the 10-bit worker space fully populated that is 1024 × 4.1M ≈ 4.2 billion IDs/second across the fleet, all without a single lock or network hop between nodes.
Compare that to a database sequence capped at 10,000–50,000 IDs/second before the hot row melts — Snowflake buys two to three orders of magnitude by moving coordination out of the request path entirely.
The bit budget is a fixed pie, and you can re-slice it. Need more than 1024 nodes? Steal bits from the sequence field. Need more than 4M IDs/second/node? Steal from the worker field. Need more than 69 years? Widen the timestamp and shrink something else. Every design is a negotiation inside 63 usable bits.
UUIDv4 vs Snowflake vs DB sequence
| Scheme | Width | Uniqueness | Sortable? | Index locality | Coordination |
|---|---|---|---|---|---|
| UUIDv4 | 128-bit | Random, collision ~impossible | No | Poor — random inserts, page splits | None |
| Snowflake | 64-bit | (time, worker, seq) | Roughly, k-sorted | Good — appends at the tail | Worker-ID assignment only |
| DB sequence | 32/64-bit | Single source of truth | Perfectly | Excellent | Every ID (bottleneck) |
Each scheme picks its poison: UUIDv4 trades width and order for zero coordination, the DB sequence trades a bottleneck for perfect order, and Snowflake pays a one-time coordination cost — assigning worker IDs — to get most of both.
How do stateless pods get unique worker IDs?
The 10-bit worker field only guarantees uniqueness if no two live nodes share a number. Baking worker IDs into static config breaks the instant you autoscale a Kubernetes deployment from 8 pods to 800.
Lease them from a coordination service instead. On startup a pod connects to ZooKeeper or etcd, creates an ephemeral node under /workers/, and claims the lowest free slot in the 0–1023 space. The lease ties to the pod's session heartbeat: if the pod dies or partitions, the ephemeral node disappears and the ID returns to the pool — but only after a safety window longer than your maximum clock skew, so a crashed node's in-flight timestamps can never be reissued to its successor.
For smaller fleets a Redis INCR over a bounded set or a SELECT ... FOR UPDATE row lock works too — large systems reach for ZooKeeper/etcd because they hand you fencing tokens and liveness detection for free, exactly the two guarantees worker-ID leasing needs.
The deep dive interviewers always reach: what if the clock goes backwards?
Snowflake trusts the wall clock, and wall clocks lie. An NTP correction can yank the clock backward by 200 ms. If your node keeps minting, it will reuse timestamps it already burned and produce duplicate IDs — the one failure the whole system exists to prevent.
The defense is to remember the last timestamp you used, per worker, in memory:
- If the current clock is ahead of the last timestamp, proceed normally.
- If it is behind by a small amount (a few hundred ms), busy-wait until the clock catches up before minting again.
- If it is behind by a large amount, refuse to mint and raise an alert so ops can drain and restart the node before bad IDs escape.
Belt and suspenders: run chrony/ntpd in slew-only mode (-x) so corrections apply gradually instead of jumping, and read a monotonic clock plus a stored offset so time can never regress under you.
The sibling failure is sequence exhaustion: 4,097 requests in one millisecond overflow the 12-bit counter. The generator spin-waits for the next tick, resets sequence to 0, and continues — capping the node at 4.1M/second rather than ever minting past the cap and colliding.
How this shows up in interviews
"Design a unique ID generator" is a warm-up that turns sharp fast. The opening is almost always UUID vs Snowflake vs sequence — have the trade-off table in your head. From there the follow-ups probe operability: how do pods get worker IDs, what happens when the clock jumps back, what happens when the sequence overflows, how do you stay sortable across regions.
The strong signal is treating clocks as adversarial and naming a concrete policy (busy-wait, refuse-and-alert, monotonic clock) instead of hand-waving "we sync with NTP." A second differentiator is security: raw Snowflake IDs leak signup volume through their timestamp bits, so if they appear in public URLs, expose an opaque external ID — a format-preserving encryption of the value, or a mapping to a random slug — while keeping Snowflake internally for fast joins.
Further learning
- Distributed ID Generation: Twitter Snowflake — System Design Concepts walks the exact 64-bit layout and the clock-skew edge cases end to end.
- Practice it interactively: open the Unique ID Generator roadmap on YeetCode.
Related core designs that reuse these ideas: Design a URL Shortener leans on the same compact-ID and base62 questions; Design a Leaderboard needs time-ordered keys for ranking; Design a Task Scheduler reuses worker-lease coordination; and Design a Rate Limiter shares the per-node counter and clock-window reasoning.
FAQ
Why does Twitter Snowflake put the timestamp in the highest bits?
Because the integer's numeric ordering then matches chronological order — a newer ID is always a larger number, so sorting by ID sorts by time, and fresh inserts append to the right edge of a B-tree index instead of landing at random positions. That preserves the index locality and cache behavior that UUIDv4 sacrifices by being fully random.
How many unique IDs can one Snowflake node generate per second?
The 12-bit sequence field allows 4,096 IDs per millisecond — about 4.1 million per second per node. Across a full 10-bit worker space of 1,024 nodes that's roughly 4.2 billion IDs per second fleet-wide, with no coordination in the request path. Need more than 4.1M/second on one node? Re-slice the bit budget and take bits from the worker field.
What happens if a Snowflake node's clock moves backward?
Minting must stop until time recovers, or the node will reissue timestamps it already used and produce duplicate IDs. The standard fix stores the last-used timestamp in memory: on a small backward jump the node busy-waits for the clock to catch up; on a large jump it refuses to mint and alerts so operators can drain and restart it. Running NTP in slew-only mode and reading a monotonic clock prevent most jumps from happening at all.
When should I use a UUID instead of Snowflake?
Use UUIDv4 when you want zero coordination and don't care about ordering or index locality — client-generated IDs in an offline-first app, or idempotency keys where the value is opaque and short-lived. Choose Snowflake when the ID is a primary key that gets range-scanned or sorted, since its 64-bit width and time-ordering keep indexes compact and inserts sequential.
Make it stick: run this one yourself
Don't just read it — step through it interactively, one state change at a time.