Course Content
System Design Interview
31 sections · 71 lessons
Unique ID Generator: clock problems, machine IDs and follow-ups
Snowflake's uniqueness rests on two assumptions: the machine clock never goes backwards, and no two nodes share a machine identifier. Machine clocks go backwards. And machine identifiers get duplicated in exactly the unglamorous ways real deployments break.
This lesson is the deep dive and the follow-ups. The clock question comes first because it is the deep-dive question on this problem, asked nearly every time. Machine identifier assignment comes second. The layout is the answer; the follow-ups are where the discussion goes, and the last part covers the five that recur.
Why clocks move backwards
Network Time Protocol (NTP) corrections. Machine clocks drift — a few parts per million, so tens of milliseconds a day. NTP corrects them by comparing against reference servers. When the local clock is ahead, the correction moves it backwards. NTP prefers to slew — slow the clock down until it catches up — but for a large enough error it will step, jumping the clock directly.
Leap seconds. Occasionally an extra second is inserted into civil time. Historically some systems repeated a second, making the clock non-monotonic. The modern practice is smearing: spreading the extra second across many hours so the clock never repeats or jumps. That solves the leap second and does nothing about NTP steps.
Virtual machine effects. A suspended and resumed virtual machine can see time jump in either direction.
Why it breaks the identifier
Suppose a node generates identifiers at millisecond T with sequence numbers 0 to 50. The clock steps back 200 ms. The node now generates at T − 200 with sequence starting at 0 again — and it has already issued identifiers for T − 200. Duplicates, and they are silent: nothing errors, two entities share an identifier, and you discover it when a foreign key resolves to the wrong row.
Worse, sorting breaks: identifiers issued later have lower values, so "most recent first" returns the wrong order.
What implementations actually do
1. Refuse to issue. Keep the last timestamp used. If the current clock is behind it, throw an error and let the caller retry.
- For: correctness is absolute; a duplicate is never issued.
- Against: the generator is unavailable for the duration of the skew. For a 200 ms step that is fine; for a five-minute correction the node is down for five minutes.
- This is the correct default, because a brief failure is enormously better than a silent duplicate.
2. Wait for the clock to catch up. If the drift is small — under a threshold such as 5 ms — block until the clock passes the last used timestamp, then continue.
- For: invisible to callers for small corrections, which is the common case.
- Against: it blocks, and blocking for an unknown period is worse than failing fast, so it must be bounded. Beyond the threshold, fall back to option 1.
3. Keep the last timestamp and refuse to go back. Continue issuing at the last used timestamp, incrementing the sequence. Safe until the sequence for that millisecond is exhausted (4,096), then it must wait or fail. Identifiers are briefly ahead of real time, which is harmless.
The recommended combination: wait for small drift, refuse and alert for large drift, and always keep the last issued timestamp in memory so the check is possible at all. Say that combination — it is what production implementations do.
The operational half of the answer
- Monitor clock offset on every generator node and alert before it matters.
- Keep generator nodes on reliable time sources, and prefer slewing over stepping in NTP configuration.
- Alert on refusals. A node refusing to issue identifiers is a symptom worth waking someone for.
Machine ID assignment: why it matters so much
Every generator node needs a unique machine identifier, and if two nodes ever share one, the whole scheme fails silently.
Two nodes with machine identifier 7, generating in the same millisecond, will produce overlapping sequence numbers and therefore identical identifiers. Nothing errors. The duplicates flow into the database, where a primary key constraint rejects one of them if you are lucky, or a shard without that constraint accepts both if you are not.
Option 1: static configuration
Assign identifiers in a configuration file or environment variable at deploy time.
- For: trivial, no dependencies, and the identifier is stable across restarts.
- Against: it is a manual process that scales badly. Copy a configuration when cloning a host, or reuse an identifier from a decommissioned machine, and you have duplicates. It also fails in an autoscaling environment where machines appear without a human involved.
- Right when: a small, fixed fleet of long-lived machines.
Option 2: coordination through ZooKeeper or etcd
Use a distributed coordination service — ZooKeeper and etcd are the common ones — as the source of truth. On startup, a node claims the lowest unused identifier by creating a node in the service, which guarantees uniqueness through consensus.
- For: correct by construction, handles autoscaling, and the coordination service already exists in most large deployments.
- Against: a dependency at startup. If the coordination service is unreachable, new generator nodes cannot start — though existing ones keep running, so it is not in the request path.
- The lease question: should the claim be ephemeral (released automatically when the node disconnects) or persistent? Ephemeral is tidier, but a network blip can release an identifier that a still-running node is actively using, and a new node can then claim it — producing exactly the duplicate you were preventing. The safer pattern is a lease with a duration longer than any plausible blip, which the node renews, and which the node treats as authoritative: if it cannot renew, it stops issuing identifiers.
- Right when: a large or elastic fleet. This is the recommended answer.
Option 3: derive it from the environment
Some platforms provide a stable ordinal: a container orchestrator's stateful set gives each replica a fixed index (worker-0, worker-1), and that index can be the machine identifier.
- For: no extra service, uniqueness guaranteed by the platform, and the identifier is stable across restarts.
- Against: it ties the design to a specific platform, and it caps you at the number of replicas the ordinal range allows.
- Right when: you already run on a platform that provides one.
The bit-budget consequence
Ten bits gives 1,024 identifiers. If machines are ephemeral and each new one takes a fresh identifier, you exhaust the space quickly. So identifiers must be recycled, which means the assignment mechanism must know when a machine is genuinely gone — precisely what the lease in option 2 provides. If your fleet churns faster than that, spend more bits on the machine field and fewer on the sequence: cutting the sequence to 10 bits still gives 1,024 identifiers per millisecond, or a million per second per node, and buys you 4,096 machine identifiers.
Detecting a duplicate
Since the failure is silent, detect it: have each node log its assigned identifier at startup and alert when two live nodes report the same one. It is a cheap check for a failure that is otherwise found weeks later in corrupted data.
Follow-up: sortable but not guessable
Snowflake identifiers are sequential within a node, so exposing them in a URL leaks information. Given /orders/1543219876543210, an attacker can decode the timestamp, subtract a day's worth of milliseconds, and estimate your daily order volume — a real competitive-intelligence leak — and can enumerate nearby identifiers to probe for resources they should not see.
Three responses, and the third is the honest one:
- Two identifiers per entity. A sortable internal identifier for storage and joins, and a separate random public identifier exposed in URLs, with an index mapping between them. Costs an index; solves the problem cleanly.
- Encrypt the identifier for display. A format-preserving or block cipher over the 64-bit value produces an opaque public form that decrypts back to the internal one. No extra index, but the key becomes a thing you must manage and rotate.
- Authorise properly. Guessable identifiers are only a vulnerability if knowing an identifier grants access. Every request must check authorisation regardless. Obscurity is a defence in depth, never the control.
Say all three, and say that the third is not optional.
Shorter identifiers for URLs
A 64-bit integer is 19 decimal digits — too long for a short link. Encode it in base 62 (digits, lowercase, uppercase) and 64 bits becomes about 11 characters. If you need 7 characters, you need a smaller number space: 62^7 ≈ 3.5 × 10^12, which is a different generator with a different uniqueness argument. Generating the short key, in Section 10, works that case through.
ULID and UUIDv7
Two modern alternatives that solve UUIDv4's sorting problem while keeping its no-coordination property.
ULID (universally unique lexicographically sortable identifier): 128 bits — a 48-bit millisecond timestamp plus 80 random bits — encoded as 26 characters in a base-32 alphabet that sorts lexicographically in the same order as the underlying value. So you get time ordering and string sorting together.
UUID version 7: the same idea inside the standard UUID format — a 48-bit millisecond timestamp in the high bits, the rest random — so it works with every existing UUID column, type, and library.
| Snowflake | ULID | UUIDv7 | |
|---|---|---|---|
| Size | 64 bits | 128 bits | 128 bits |
| Coordination | Machine identifier needed | None | None |
| Time-sortable | Yes | Yes | Yes |
| Guessable | Highly | Timestamp only | Timestamp only |
| Rate per node | 4,096/ms | Bounded by randomness | Bounded by randomness |
When to choose which: Snowflake when 64 bits matters — a large table where 8 bytes per index entry is worth the machine-identifier machinery. UUIDv7 when it does not, because it removes the machine assignment problem and the clock-skew duplicate problem (both earlier in this lesson) entirely, at the cost of 8 extra bytes everywhere. For most systems being designed today, that is the better trade, and saying so is a more current answer than reciting Snowflake alone.
Two more that come up
Multi-region uniqueness. The datacentre bits handle it, as long as the region-to-bit mapping is assigned centrally and never reused. Say that the mapping is configuration, not derivation.
Backfilling identifiers for existing rows. Generating Snowflake identifiers for historical rows produces timestamps of "now", destroying the time-sortability you wanted. Either derive the timestamp field from the row's real creation time, or accept that pre-migration rows do not sort correctly and document it.