Skip to main content
Distributed ID Generatorlesson 3 of 3 · 3 min read

Clock Skew and the Alternatives

The clock

This design has exactly one dependency it does not control, and it is the machine clock. Clocks misbehave.

Time corrections step backward by milliseconds or more. A virtual machine migration jumps it. A rare leap-second bug shoves it a full second.

Follow what happens when your clock moves backward. Your generator revisits timestamps it already used, and with the same machine identifier and a reset sequence it mints duplicates of identifiers it issued moments ago.

Sit with that outcome. Duplicate primary keys, produced by the one component whose entire job was uniqueness.

Defend simply, and treat these as mandatory. Have your generator remember the last timestamp it used, and if the clock reads earlier, refuse to generate, either erroring or sleeping until the wall clock catches up. Alarm at the same time, because something is wrong with time on that machine.

Run your time synchronisation in the mode that adjusts gradually instead of stepping, on every generator host.

Look at how one variant spends the bit budget differently, using 10 millisecond units instead of one. That buys 174 years of range and eases the pressure on the sequence, at the cost of fewer identifiers per unit.

Two alternatives

Consider two alternatives that trade the clock dependency for other costs. Range allocation hands each server a block of, say, a thousand identifiers from a coordinator, and servers issue locally from their block, returning when it runs out.

Weigh it honestly. No clock at all, and near-zero coordination once you amortise it over the block. Your identifiers are only coarsely ordered, and a crashed server's unused block leaves permanent holes, fine until somebody treats identifiers as a count.

Push the problem into your database instead, the other direction. One well-known photo product built generation inside Postgres itself, packing time, shard and sequence bits in a stored function, with no separate service to operate.

Close an interview with the decision rule. Wanting them sortable, at a high rate, points to the Snowflake layout with clock guards. Not trusting clocks points to range allocation. And a small system should keep the database sequence it already has.

the shape of it
NTP stepclock jumps back 3sMachine clockGeneratorlast_ts = 10:00:05Refuse + alertwait for clockDuplicate IDswithout the guardsets 10:00:02now < last_tsguarded pathunguarded path
step 1 of 3
When the clock reads earlier than the last issued timestamp, a guarded generator halts and alarms instead of re-minting IDs from the past.

Worked example

A payments company runs Snowflake-style generators on 40 hosts. During a maintenance window, an engineer fixes a 3-second clock drift on one host by forcing an NTP step correction, and the clock jumps backward 3 seconds while the generator is serving traffic. The naive implementation has no last-timestamp guard, so for the next 3 seconds it re-mints timestamp values from the recent past, and 7 transaction IDs collide with IDs issued minutes earlier. The duplicates surface as unique-constraint violations in the ledger, the one place the company is lucky, because a constraint catches what the generator did not. The remediation ticket reads like the textbook: track last-issued timestamp and refuse to go backward, alert on refusal, switch NTP to slew-only on generator hosts, and add a canary that compares each host's clock against three peers every minute.

Distributed ID Generator: wrapping up

In the real world

  • 01Twitter open sourced Snowflake in 2010 after outgrowing MySQL-based tweet ID generation, and the 41-10-12 bit layout it published is the reference design nearly every 64-bit ID scheme since has riffed on.
  • 02Discord uses Snowflakes with a 2015 epoch for every API object and documents the bit layout publicly, so clients derive creation timestamps and build time-range queries from IDs alone.
  • 03Instagram published its sharded ID design in 2011: 41 bits of time, 13 bits of logical shard, and a per-shard sequence, generated inside Postgres by a PL/pgSQL function to avoid running a separate ID service.
  • 04Flickr's 2010 ticket server post described two MySQL servers issuing IDs via auto-increment with offsets 1 and 2 and increment 2, a deliberately unsophisticated design that survived because either box could carry the load alone.
  • 05UUIDv7 was standardized in RFC 9562 (2024) with a leading Unix timestamp precisely because random UUIDv4 primary keys fragment B-tree indexes, bringing Snowflake's key insight, time in the high bits, to the UUID format.

Questions people ask

Why do random UUIDs make bad database primary keys?

Two costs: size and locality. At 128 bits they double the storage of every key, index entry, and foreign key versus a 64-bit ID. Worse, random values insert at random positions in the B-tree, so under write load the working set becomes the entire index instead of the hot rightmost pages, causing page splits and buffer pool thrashing. Time-ordered formats like UUIDv7 fix the locality problem; the size cost remains.

What happens if two Snowflake generators end up with the same machine ID?

They silently mint colliding IDs whenever they generate in the same millisecond, and nothing detects it at generation time; you find out from unique-constraint violations or corrupted references downstream. This is why machine ID assignment must be enforced, not conventional: lease it from ZooKeeper or etcd at startup, or derive it from an orchestrator-guaranteed identity like a StatefulSet ordinal, and refuse to start without one.

Do I need a distributed ID generator, or is my database sequence fine?

If you have one writer database, the sequence is fine and simpler than anything in this course; use BIGINT and move on. The distributed generator earns its complexity when you shard writes across databases, generate IDs in services before any database write, or need IDs at rates a single sequence cannot serve. Adopting Snowflake before sharding is prepaying for a problem you may never have.

Quick review

UUID v4:
128-bit random. Zero coordination, globally unique. Not sortable. Poor as DB primary key (random inserts = B-tree fragmentation)
Database auto-increment:
simple, sortable. Single point of failure. Doesn't scale to multiple writers
Twitter Snowflake:
64-bit = 1 sign bit | 41-bit timestamp (ms since epoch, 69 years) | 10-bit machine ID | 12-bit sequence (4096 IDs/ms/machine)
Snowflake properties:
roughly time-sortable (great for DB primary keys), no coordination needed per ID generation, 4096 IDs/ms per machine
Segment's approach:
DB-assigned ranges. Service pre-allocates range of 1000 IDs. Serve locally until exhausted, then get new range
Redis INCR:
atomic counter. Single node = SPOF. Use Redis cluster with range pre-allocation
Clock skew problem:
Snowflake breaks if machine clock goes backward. Use NTP + refuse to generate on backward clock
the trade-off

Snowflake requires synchronized clocks (NTP). UUID is coordination-free but non-sortable. Bad for DB performance.

in the room

Any system needing unique identifiers at scale. Tests knowledge of time-based IDs, sortability, and coordination-free design.