cld-toys › Toys › unique-id-generator

Commentary: unique-id-generator

A Snowflake ID is sold as collision-free with zero coordination. It coordinates with other machines for free and with its own past not at all — and under a liveness probe, Twitter's "refuse to generate an id" produces 390 duplicates where doing nothing produces 400. A study guide for snowflake.py.

unique-id-generator/ on GitHub·the source with line numbers, for reading rather than downloading
How to read this This is the only documentation the toy has — read it with snowflake.py open beside you. snowflake.py is the toy itself (244 lines: the generator, a simulated wall clock, and a request loop); demo.py runs the seven experiments this page is built on; test_snowflake.py pins every number on this page across 32 tests. Time is an argument: next_id(now) never reads a clock, the module has no imports at all — not even time — and a tilNextMillis block is an exception the caller resolves by advancing the simulation's own counter — so a 50 ms stall costs no real seconds and the whole demo runs in under a second, byte-identically on every machine. The one RNG (§6.8 only) is seeded. No dependencies, stdlib only. Every transcript below was captured on macOS 26.5.2 (Darwin 25.5.0, arm64 Apple Silicon), Python 3.15.0a8 (main, Apr 14 2026, 14:20:41) [Clang 22.1.3].
cd unique-id-generator
python3 demo.py             # the aha (§6) — about half a second
python3 test_snowflake.py   # pins every number this page claims
Contents
  1. Orientation
  2. The problem this mechanism exists to solve
  3. Background you need
  4. The mental model
  5. Reading the source
  6. The demo, and what it proves
  7. Design decisions and roads not taken
  8. What's simplified vs. the real thing
  9. Check yourself
  10. Further reading

1. Orientation

This toy is a Snowflake-style unique ID generator: the thing that hands out primary keys in a system with no central sequence. One 64-bit integer, three fields:

[ 1 unused ][ 41 timestamp ms ][ 10 machine id ][ 12 sequence ]

The mechanism it teaches is not the bit packing — that is one shift and one mask. It is the question the packing quietly leaves open: two IDs from the same machine in the same millisecond differ only in the sequence counter, and the sequence counter is only correct because the generator remembers which millisecond it was last in. That memory is self.last_ts, it lives in RAM, and every "no coordination needed" claim rests on it.

Three policies for a clock that goes backwards, all implemented and all priced against one trace:

The shortest version of the result:

from snowflake import BLIND, REFUSE, CLAMP, run

for policy in (BLIND, REFUSE, CLAMP):
    print(policy, run(policy, fail_after=10).dupes)
# blind 400
# refuse 390
# clamp 0

fail_after=10 is a liveness probe: ten consecutive failures and the supervisor replaces the process. That one line is the whole toy.

By the end you should be able to derive 400 and 390 from the trace, say which line makes clamp immune to the probe, explain why persisting the generator's memory is still not enough, and work out how much clock error your own throughput can absorb before the generator stalls.

2. The problem this mechanism exists to solve

You need a primary key. A database sequence gives you one, and it gives you a round trip and a single point of failure with it. A random UUID avoids both and costs you 128 bits and index locality — random keys scatter inserts across a B-tree instead of appending to one hot leaf. Snowflake IDs are the answer that says: 64 bits, no round trip, and sorted enough that inserts stay local.

The competing goals that make more than one design defensible:

These pull against each other in exactly one place. Coordination-free means "decided from local state". Local state means "lost on restart". And a restart is not a rare event — it is what a healthy production system does automatically, in response to failures, which is the loop this toy closes.

3. Background you need

ConceptWhere it's used in the toyLink
Bit packing and field masks pack / unpack; the mask is why machine 1025 is machine 1 Discord: snowflakes
Wall clock vs. monotonic clock Wall.read(t) — true time t is the simulation's, the generator only sees the wall chrony: makestep
NTP step vs. slew step_at / step_ms: one discontinuous jump backwards chrony: makestep
Sequence as an intra-millisecond tie-break the now == self.last_ts branch of next_id RFC 9562 §6.2, monotonicity
tilNextMillis / spin-to-next-ms Spin, raised by _overflow, resolved by run Twitter IdWorker.scala
Liveness probe / failure threshold run(fail_after=K): K consecutive refusals and the process is replaced Kubernetes: probes
Process state vs. configuration _spawn: machine_id survives a restart, last_ts does not MongoDB ObjectId
Leap smear why production clocks mostly don't step, which is why this bug is rare and awful Google: leap smear

Four rows carry the result. The generator's whole defence lives in comparing a wall clock reading against remembered state; an NTP step is what makes that comparison fire; a liveness probe is what deletes the state mid-defence; and the split between configuration and process state is what decides whether a restart is harmless or catastrophic.

4. The mental model

One ID, and the two variables that decide whether it is unique:

next_id(now) | now < last_ts? -----+----- now == last_ts? -----+----- now > last_ts the clock went back | same millisecond | a new millisecond | | | | | blind: use it clamp: now = seq += 1 (4096 used) seq = 0 refuse: raise last_ts (mask 4095) spin/borrow last_ts = now | | | | | +-----------------+------------+----------------+------------+ | id = (ts - epoch) << 22 | machine_id << 12 | seq 64 bits: 0 tttttttttttttttttttttttttttttttttttttttttt mmmmmmmmmm ssssssssssss ^ 1 unused ^ 41 timestamp ms ^ 10 machine ^ 12 sequence state that survives a restart: machine_id, epoch, the bit widths (config) state that does not: last_ts, seq (RAM)

And the trace the whole page runs on — one machine, 8 IDs/ms, and one 50 ms step backwards at true time 100:

true time 0 .......... 99 100 ................ 149 150 ......... 200 wall reads 0 .......... 99 50 ................ 99 100 ......... 150 ^ ^ | | NTP steps back 50 ms the wall catches up with every ms from 50 to 99 itself; from here on every is about to happen twice millisecond is fresh again

Everything in §6 is a different answer to "what should the generator do during that middle stretch, and what happens if it dies in there."

5. Reading the source

5.1 The two kinds of state

snowflake.py · lines 54–69
class Snowflake:
    def __init__(self, machine_id, epoch=0, ts_bits=41, mid_bits=10,
                 seq_bits=12, rewind=BLIND, overflow=SPIN):
        assert 1 + ts_bits + mid_bits + seq_bits == 64
        self.machine_id = machine_id
        self.epoch = epoch
        self.ts_bits, self.mid_bits, self.seq_bits = ts_bits, mid_bits, seq_bits
        self.seq_mask = (1 << seq_bits) - 1      # 4095 slots per ms at 12 bits
        self.mid_max = (1 << mid_bits) - 1       # 1023 at 10 bits
        self.rewind = rewind
        self.overflow = overflow
        # The amnesia. Everything above came from config and survives a
        # restart; these two are the state, and they do not.
        self.last_ts = -1
        self.seq = 0
        self.refusals = self.spins = self.borrowed = 0

The comment is the argument of the whole page, so it is worth being precise about why the line above it and the line below it are different. machine_id comes from a config file, an environment variable, a pod ordinal — some external authority that remembers. last_ts comes from the last call to next_id and lives nowhere else. Both are equally load-bearing for uniqueness. Only one of them survives kill -9.

self.last_ts = -1 is the specific line that makes a fresh process believe whatever the clock tells it. Every result in §6.3 and §6.4 is that line.

5.2 The three branches

snowflake.py · lines 81–103
def next_id(self, now):
    """`now` is a wall-clock reading in ms, supplied by the caller.

    Returns (id, ts); raises Exhausted or Spin when a policy says so.
    """
    if now < self.last_ts:
        # The clock went backwards. Somebody has to decide what that means.
        if self.rewind == REFUSE:
            self.refusals += 1
            raise Exhausted("clock moved backwards by %d ms"
                            % (self.last_ts - now))
        if self.rewind == CLAMP:
            now = self.last_ts       # pin to the highest ts ever issued
        # BLIND: fall through and cheerfully reuse an old millisecond

    if now == self.last_ts:
        self.seq = (self.seq + 1) & self.seq_mask
        if self.seq == 0:            # wrapped: 4096 IDs spent in this ms
            return self._overflow(now)
    else:
        self.seq = 0
        self.last_ts = now
    return self.pack(self.last_ts, self.seq), self.last_ts

Taking the clock as an argument is the design decision that makes this toy possible: now is data, so a 50 ms NTP step is a subtraction in the caller rather than a mocked syscall, and the boundary cases (now exactly equal to last_ts) are reachable on purpose instead of by luck.

Three things about the strictness of <. First, clamp is written as an assignment to a local now, not to self.last_ts — the generator does not believe the clamped time, it just refuses to go below it, and the very next line handles the resulting equality naturally. Second, the else branch resets seq to 0, which is what makes an ID reproducible: same millisecond, same starting sequence, same ID. That is the reason duplicates happen, and §6.6 deletes the line to prove it.

Third, and least obvious: now == self.last_ts is itself a defence, and it is the one nobody names. A rewind that lands exactly on the millisecond the generator is already in never reaches the backwards branch at all — it takes the increment path and keeps counting. That single == is why a 1 ms rewind costs zero duplicates under every policy including blind (§6.7), and why refuse refuses rate × (rewind − 1) requests rather than rate × rewind.

5.3 Running out of sequence

snowflake.py · lines 105–113
def _overflow(self, _now):
    if self.overflow == BORROW:
        self.borrowed += 1
        self.last_ts += 1            # a millisecond that has not happened
        self.seq = 0
        return self.pack(self.last_ts, self.seq), self.last_ts
    self.spins += 1                  # tilNextMillis, as an exception
    self.seq = self.seq_mask
    raise Spin(self.last_ts + 1)

Twitter's version blocks here in a while loop until System.currentTimeMillis moves on. Blocking is untestable and slow, so the toy raises Spin(until) and makes the caller advance time — same semantics, no seconds spent, and the stall becomes a number the demo can print instead of a delay a reader has to believe. self.seq = self.seq_mask before raising is the important line: the generator stays wrapped, so a retry that somehow lands in the same millisecond increments back to 0 and re-raises rather than handing out sequence 0 twice.

borrow is the third answer nobody implements: issue a millisecond that has not happened. It never stalls, and it pays by letting timestamps run ahead of the clock — measured at 49 ms ahead in §6.7.

5.4 The clock the generator is allowed to see

snowflake.py · lines 116–130
class Wall:
    """wall(t) = t, except from `step_at` onwards, where it jumps back `step_ms`.

    `t` is the simulation's own monotonic millisecond counter -- true time,
    which no process can read. The generator only ever sees `read(t)`, so an
    NTP step backwards is one subtraction rather than a mocked syscall.
    """

    def __init__(self, step_at=None, step_ms=0):
        self.step_at, self.step_ms = step_at, step_ms

    def read(self, t):
        if self.step_at is not None and t >= self.step_at:
            return int(t - self.step_ms)
        return int(t)

Two clocks, and the distinction between them is the entire experimental apparatus. t is true time — a float, monotonic, known only to the simulation. read(t) is what a process gets from System.currentTimeMillis: an integer, and a lie for a 50 ms window. Keeping both lets the transcript say "at true t=101.125 the wall read 51", which is the sort of sentence you cannot write from inside a real process at all.

5.5 A restart, as one function

snowflake.py · lines 170–183
def _spawn(cfg, store, start_rule, cls):
    """A fresh process. Config survives; `last_ts` is whatever startup says.

    RAM is the real generator: a new process starts at -1 and believes the
    clock. RESUME and SKIP are the durable variants -- see the commentary for
    why the obvious one of the two is still wrong. `cls` exists so a demo can
    run a one-line variant of the generator down the identical trace.
    """
    gen = cls(**cfg)
    if start_rule == RESUME and store["hwm"] >= 0:
        gen.last_ts = store["hwm"]
    elif start_rule == SKIP and store["hwm"] >= 0:
        gen.last_ts = store["hwm"] + 1
    return gen

A restart is modelled as cls(**cfg) — the config dictionary is reapplied verbatim, and nothing else crosses. That is the honest model: the pod spec still says machine_id=7, and the heap is gone. The two durable rules poke last_ts from outside rather than living in __init__, which keeps them visibly external to the mechanism; a generator that reads a store at startup is a different thing from a generator, and §6.4 is about what that difference costs.

5.6 The loop that closes

snowflake.py · lines 216–235
while True:
    w = wall.read(t)
    try:
        ident, _ts = gen.next_id(w)
    except Exhausted:
        records.append((t, w, None, REFUSED))
        consecutive += 1
        if fail_after and consecutive >= fail_after:
            borrowed += gen.borrowed
            gen = _spawn(cfg, store, start_rule, cls)
            restarts += 1
            restart_log.append((t, w))
            consecutive = 0
        t_free = t
        break
    except Spin as spin:
        nxt = wall.reaches(spin.until, t)
        stalled += nxt - t
        t = nxt
        continue

consecutive is a liveness probe in one variable: count failures, reset on success, replace the process at the threshold. Kubernetes' own example sets failureThreshold: 3 and its docs put it plainly — "if a container fails its liveness probe more times than the configured tolerance, the kubelet restarts that container."

Note what the except Exhausted branch does not do: retry. A refusal is a failed request, it is recorded as one, and the caller moves on. That matters because it means the refusal is visible to the supervisor, which is the only reason §6.3 happens at all. A generator whose failures are swallowed by a retry loop three layers up is a generator whose defence never triggers the probe — and, per the same argument, never gets fixed either.

6. The demo, and what it proves

The trace, throughout: machine 7, 8 IDs/ms for 200 ms — 1600 requests offered — and one 50 ms NTP step backwards at true t=100 ms.

6.1 What is actually in the 64 bits

next_id(50) -> 209743872 0000000000000000000000000000000000001100100000000111000000000000 unpacks to ts=50 machine=7 seq=0 fields: 41 ts bits, 10 machine bits (1024 machines), 12 seq bits (4096 IDs per ms per machine) 41 bits of ms = 2199023255552 ms = 69.68 years epoch 2010-11-04 -> the timestamp field overflows 2080-07-10 machine_id=1 -> 2097156096 machine_id=1025 -> 2097156096 identical: True (1025 & 1023 = 1) The field is masked, not checked. Machine 1025 is machine 1.

241 milliseconds is 69.68 years, so a scheme anchored at Twitter's epoch of 1288834974657 ms (2010-11-04) overflows its timestamp field on 2080-07-10. Note the shape of that number: it is a property of the bit budget, and every bit you move to another field costs you half of it.

The aliasing is the same arithmetic seen from the machine field. machine_id & 1023 is a mask, not a check, so the 1025th machine you provision is silently the 1st, and the two of them will hand out identical IDs forever with no error anywhere. Both facts are pinned by test_snowflake.py::forty_one_bits_of_milliseconds_runs_out_in_2080 and ::the_machine_field_is_masked_not_checked.

6.2 The clock steps back

rewind policy issued refused DUPES stall(ms) blind 1600 0 400 0.000 refuse 1208 392 0 0.000 clamp 1600 0 0 0.000 first 3 collisions under blind, as (t_first, t_again, id): t= 50.000 and t=100.000 -> 209743872 (ts=50 machine=7 seq=0) t= 50.125 and t=100.125 -> 209743873 (ts=50 machine=7 seq=1) t= 50.250 and t=100.250 -> 209743874 (ts=50 machine=7 seq=2) total 400 = rate 8/ms x rewind 50 ms = 400 refuse pays 392 refusals for its 0 duplicates; clamp pays nothing.

Derive the 400. The wall clock re-covers milliseconds 50 through 99 — 50 milliseconds of the past — and the generator, having reset seq to 0 on each apparently-new millisecond, replays the identical sequence in each: 8 IDs/ms × 50 ms = 400. The very first collision is the whole story in one line: ID 209743872 is (ts=50, machine=7, seq=0), and it was issued at true t=50.000 and again at true t=100.000, 50 ms apart, by a generator that never saw an error.

Derive the 392. Not 400, and the difference is instructive. Refusals run while the wall reads strictly below last_ts = 99, which is true from true t=100 up to but not including t=149, where the wall reads exactly 99 again. That is 49 ms at 8/ms = 392, and 1600 − 392 = 1208 issued. The general form is rate × (rewind − 1) — verified at rewinds of 2, 5, 10 and 50 ms in ::refuse_buys_zero_duplicates_with_392_refusals. The missing millisecond is the == branch from §5 quietly doing its job.

So far this reads as a clean ranking: blind is broken, refuse is principled and expensive, clamp is free. That ranking survives exactly as long as nobody is watching the process.

6.3 The headline: the defence bought 10 IDs

Nobody restarts a process at a random instant. They restart it because it started failing. Add a liveness probe — K consecutive failures and the supervisor replaces the process — and the replacement's last_ts is -1.

policy K=0 K=1 K=3 K=10 K=50 restarts blind 400 400 400 400 400 0 0 0 0 0 refuse 0 399 397 390 350 0 1 1 1 1 clamp 0 0 0 0 0 0 0 0 0 0 The defence that refuses to serve produced 390 duplicates. Doing nothing at all produced 400. It bought 10.
The headline The defence that refuses to serve produces 390 duplicates where doing nothing produces 400. For its 392 refused requests, refuse bought ten IDs.

The closed form is dupes = 400 − K, exact at every K, and the demo derives the K=10 row:

derivation, K=10: refusals start the instant the clock steps, at true t=100.000, one every 1/8 ms. The 10th is 9 intervals later: 100.000 + 9/8 = 101.125 (measured restart: t=101.125, wall read 51) the next request arrives at t=101.250, and every one from there until the wall recovers at t=150.000 is a duplicate: (150.000 - 101.250) x 8 = 390 closed form, dupes = 400 - K: K=1 400 - 1 = 399 measured 399 K=3 400 - 3 = 397 measured 397 K=10 400 - 10 = 390 measured 390 K=50 400 - 50 = 350 measured 350

Read 400 − K again, because the algebra is the point. A refusal is one request that did not become a duplicate. The policy converts exactly K of them, and then the Kth one triggers the restart that deletes the state making it correct. Everything after that is blind. The defence's total lifetime value is K IDs, whatever K is, and K is a number in a YAML file that nobody chose with this in mind. test_snowflake.py::the_supervisor_turns_the_refusal_into_400_minus_k holds it at K ∈ {1, 2, 3, 5, 10, 25, 50, 100, 200, 390, 391, 392}; at K = 393 there aren't 393 refusals to be had, no restart fires, and the count drops back to 0.

The asymmetry in the table is the sharp bit. refuse is the only policy that causes its own restart: it converts a clock problem into a stream of request failures, and a stream of request failures is exactly what a supervisor is built to act on. clamp never refuses, so no value of K can fire — its restarts column is 0 0 0 0 0 — and blind never refuses either, so the probe cannot help it. A defence that signals failure has enrolled the platform in its own destruction; a defence that stays quiet is invisible to the platform in both directions.

6.4 The amnesia is one integer, and persisting it is not enough

Restart the process at a chosen instant instead — a deploy at true t=120, 20 ms into the rewound window — and compare three startup rules.

restart at true t=120 ms, 20 ms into the rewound window startup rule blind refuse clamp last_ts = -1 400 240 240 last_ts = hwm 400 7 167 last_ts = hwm + 1 400 0 0 why `last_ts = hwm` still collides, derived: the high-water mark at the restart is ms 99. The old generator spent that millisecond as seq 0..7 its 8 genuine requests during true t=99..100 seq 8..167 20 ms of clamped requests, 8/ms 8 + 20x8 = 168 slots, so seq 0..167 were used. The replacement resumes AT ms 99 with seq=0 and reissues seq 1..167: 167 duplicates. Measured: 167. The millisecond is not the unit of uniqueness; the (ms, seq) pair is.

Row A (last_ts = -1) is the real generator, and it makes refuse and clamp indistinguishable: 240 duplicates each, because the replacement believes the rewound clock and reissues wall milliseconds 70 through 99 — 30 ms × 8 = 240. Whatever the old process had decided about the clock died with it.

Row B is the obvious fix — write the high-water mark down, read it back at startup — and it is still wrong: 167 duplicates under clamp. The derivation is in the transcript. The old generator did not merely reach millisecond 99; it spent 168 sequence slots inside it (8 genuine requests, then 20 ms of clamped ones at 8/ms). A replacement that resumes at 99 with seq = 0 walks straight back over 1 through 167. The unit of uniqueness is not the millisecond; it is the (ms, seq) pair, and a durable record of only half the pair is a durable record of nothing.

The refuse column of row B is the same error at a smaller size: 7. That generator refused everything through the window rather than clamping, so it had spent only seq 0–7 in ms 99, and the replacement reissues 1 through 7.

Row C (last_ts = hwm + 1) skips the recorded millisecond outright and reaches 0 — but only with clamp; row C with blind is still 400, because rule C fixes the restart and blind was never defending in the first place. And the price of row C is a durable write on the ID path, at which point the "zero coordination" that justified the whole design is gone. That is the honest end of this thread: you can have coordination-free IDs, or you can have IDs that survive a restart during a clock rewind, and the toy cannot find you a third option.

6.5 One character: < vs <=

generator policy issued refused DUPES < as written refuse 1208 392 0 < as written clamp 1600 0 0 <= variant refuse 150 1450 0 <= variant clamp 1600 0 0

Widening the comparison is inert for clamp — clamping now to last_ts when they are already equal is a no-op — and catastrophic for refuse: issued collapses from 1208 to 150. Derive the 150: the variant refuses any request that shares a millisecond with the previous one, so at 8 IDs/ms only the first request of each millisecond survives. That is 100 IDs over the first 100 ms, then nothing at all until the wall recovers at t=150, then one per millisecond for the remaining 50: 100 + 50 = 150.

The same edit is worth nothing and worth everything depending on which policy is switched on, which is a decent argument for the toy's shape: policies as data, one trace, and the differences read straight off the table.

6.6 One line deleted: the free-running sequence

generator policy issued refused DUPES seq resets blind 1600 0 400 free-running seq blind 1600 0 0

Delete self.seq = 0 from the new-millisecond branch and the counter runs free across milliseconds. The rewind now costs zero duplicates with no clock defence whatsoever — blind goes from 400 to 0 — because the replayed millisecond 50 gets sequence numbers 400-something instead of 0 through 7.

That result is real and it is not a fix, which the demo then demonstrates rather than asserts:

longer trace: 8 IDs/ms, 200 ms rewind at t=600, restart at t=R R R - rewind DUPES 700 500 0 704 504 0 708 508 0 712 512 704 716 516 0 720 520 0 712 - 200 = 512 exactly, and the collision is the whole rest of the window: wall ms 512..599, 8 IDs each = 704. The ordinary generator on that same trace: 1600 duplicates.

A free-running counter is not safe across a restart, it is phase-shifted. The old process's sequence is rate × t mod 4096; the replacement's starts at 0. They agree whenever the restart sits a whole number of 4096 / 8 = 512 milliseconds after the step, and at R − rewind = 512 exactly, every remaining millisecond of the window duplicates: (800 − 712) × 8 = 704. Neighbouring restarts, 4 ms either side, cost nothing at all. A safety property that depends on a modular coincidence is not a safety property, and in the 200 ms headline trace the coincidence is simply unreachable — which is how a bug like this stays hidden.

6.7 The boundary: the sequence field is a clock-error budget

clamp never stalls for free. It absorbs a rewind by spending sequence numbers, so how much rewind a generator can survive is a bit budget question, not a clock question.

50 ms rewind, sweeping the request rate (12 seq bits = 4096 slots/ms) IDs/ms issued DUPES stall(ms) max stall 8 1600 0 0.000 0.000 40 8000 0 0.000 0.000 79 15800 0 0.000 0.000 80 16000 0 0.000 0.000 81 16200 0 0.432 0.432 82 16400 0 1.049 1.049 100 20000 0 10.040 10.040 400 80000 0 44.760 40.760 slots needed = rate x (rewind + 1) ; available = 4096 51 x 80 = 4080 <= 4096 51 x 81 = 4131 > 4096

80 IDs/ms is free and 81 stalls. Every request in the 50 ms window is pinned to one millisecond — the last one issued before the step — and that millisecond had already spent its own rate slots, so the requirement is rate × (rewind + 1) ≤ 4096. 51 × 80 = 4080 fits; 51 × 81 = 4131 does not, and the generator blocks for 0.432 ms waiting for a millisecond it has run out of room in. Held from the other side, at a fixed 100 IDs/ms, a 39 ms rewind is free and 40 ms stalls (100 × 40 = 4000 ≤ 4096 < 4100).

Which turns the bit split into a different question than it is usually presented as:

layout machines seq slots stall(ms) DUPES 41/11/11 2048 2048 31.520 0 41/10/12 1024 4096 10.040 0 41/9/13 512 8192 0.000 0 41/8/14 256 16384 0.000 0

Same 50 ms rewind, same 100 IDs/ms, four ways to spend the 63 bits. Moving one bit from the machine id to the sequence takes a 10.040 ms stall to 0.000 and halves the fleet cap. So 41/10/12 is not "1024 machines at 4096 IDs/ms" — it is "1024 machines that can absorb 40 ms of clock error at 100 IDs/ms". The sequence field is sized by your clock discipline as much as by your throughput, and nobody writes it down that way.

borrow opts out of the trade entirely and pays elsewhere: 0 stall, 0 duplicates, and timestamps running 49 ms ahead of the wall clock. IDs from the future sort before events that haven't happened, which is a different bug in a different system.

Where the whole effect vanishes:

rewind ms blind DUPES refuse refusals 0 0 0 1 0 0 2 16 8 3 24 16

A rewind of one millisecond is free for everybody, including the generator with no defence at all. The step lands on the millisecond the generator is already in, now == last_ts holds, and the increment branch handles it — so blind duplicates nothing and refuse refuses nothing. Duplicates are rate × rewind only from 2 ms up. Sub-millisecond clock error, the kind a slewing NTP daemon actually produces, is not a Snowflake problem at all; this whole page is about the step, and steps are rare, which is precisely why the defence against them is untested in every system that has one.

6.8 The claim that survives

The other half of the sales pitch, measured rather than assumed:

machines IDs/ms IDs pairs mis-ordered worst error 1 10 2000 1999000 0 0.0% 0.000 ms 8 1 1600 1279200 2881 0.2% 0.983 ms 8 100 16000 127992000 2825837 2.2% 0.999 ms 64 10 32000 511984000 5029472 1.0% 0.999 ms

With perfectly synchronised clocks and no rewind at all, 0.2% to 2.2% of ID pairs come out in the wrong order, and every single error is under one millisecond — 0.999 ms at worst, because sub-millisecond ordering is simply not represented in the ID. One machine inverts nothing, ever. "Roughly sortable, where roughly means ±1 ms" is true, cheap, and the least interesting fact about the design; it is in the demo so that it can be dismissed with a number rather than a shrug.

7. Design decisions and roads not taken

Time as an argument, not a syscall. next_id(now) is slightly awkward to call and buys three things that this toy cannot do without: the demo runs in half a second instead of sleeping through a 50 ms stall; the output is byte-identical on every machine (::the_same_trace_gives_byte_identical_records); and the exact boundary — a rewind that lands precisely on last_ts — is reachable on purpose, which is where §6.7's vanishing point came from. A toy that called time.time() could not have found it.

tilNextMillis as an exception, not a loop. Blocking would be one line shorter and untestable. Raising Spin(until) moves the waiting into the caller, where it becomes a measured stall in milliseconds — which is what makes the 80-vs-81 boundary a table instead of a stopwatch.

One machine, not a fleet. The machine id is the part of Snowflake that gets all the attention and it is the boring part: it comes from config, it never changes, and it makes cross-machine collisions impossible by construction (as long as nobody provisions the 1025th machine). Simulating a fleet would have added parameters and taught nothing the single generator doesn't. Every interesting failure here is a generator colliding with its own past.

Sortability was measured and rejected as the aha. §6.8 is what remains of a headline the backlog originally proposed. It is true — which is exactly why it makes a bad toy. Nothing is surprising in a result that matches the marketing.

Three rewind policies, not one. Shipping only clamp would have hidden the result: the page's argument is comparative, and needs refuse present in working order to show that a worse-behaved generator produces fewer duplicates under a supervisor. blind is not a strawman either — it is what you get when you write the obvious version, and it is the baseline that makes "it bought 10" a sentence.

A liveness probe, rather than a random restart. A restart at a random instant is a coincidence and reads as one. A restart caused by the refusals is a mechanism, and it closes the loop between the defence and the harm. fail_after is eight lines inside run and it is the reason the toy exists.

No UUIDv7 implementation, though it is the obvious rival. RFC 9562 puts 48 bits of Unix ms at the front and 74 bits of randomness behind, so a restart re-randomises rather than resuming — the failure in §6.4 is structurally impossible. It also costs 128 bits, and its own §6.2 guidance for the monotonic-counter variants is that an implementation "MAY reuse the previous timestamp and increment the previous counter" on a rollback, which is clamp under another name. Implementing it would have doubled the toy to re-derive one conclusion.

No base-62 encoding, no ZooKeeper machine-id assignment, no threads. Each is a real part of a production ID service and none of them changes a number on this page.

8. What's simplified vs. the real thing

9. Check yourself

Answer before expanding. Every answer is derivable from the source, and each one was verified by running it.

Question 1

With a probe set to failureThreshold: 3 — the Kubernetes default — how many duplicate IDs does the refuse policy produce on this trace, and how many does it prevent?

Answer

397 produced, 3 prevented. dupes = 400 − K with K=3. The three refused requests are the only ones the policy converts; the third triggers the restart at true t=100 + 2/8 = 100.250, and every request from t=100.375 to t=150 collides: (150 − 100.375) × 8 = 397. Run run(REFUSE, fail_after=3).dupes.

Question 2

The clock steps back by exactly 1 ms instead of 50. Which policy wins?

Answer

None of them — they are indistinguishable, because there is nothing to defend against. The step takes the wall from 100 to 99, which is exactly last_ts, so now < self.last_ts is false and the now == self.last_ts branch increments the sequence as it would have anyway. blind produces 0 duplicates, refuse refuses 0 requests (§6.7). The first millisecond of any rewind is absorbed by the equality branch, which is also why refuse refused 392 rather than 400 in §6.2.

Question 3

Your service does 200 IDs/ms on one machine with the standard 41/10/12 layout. What is the largest clock rewind clamp absorbs without stalling?

Answer

19 ms. The budget is rate × (rewind + 1) ≤ 4096, so rewind ≤ 4096/200 − 1 = 19.48, and rewind is an integer number of milliseconds. Measured: 18 and 19 ms stall 0.000 ms, 20 ms stalls 0.520 ms, 21 ms stalls 1.520 ms. Going to a 41/9/13 layout doubles it to 39 ms and halves your fleet cap to 512.

Question 4

Three replicas behind a load balancer, all reading MACHINE_ID=7 from the same config map. What breaks, and when does the first duplicate appear?

Answer

Immediately, with no clock error required at all. Each replica keeps its own last_ts and seq, so all three reset seq to 0 on the same millisecond and hand out (ts, 7, 0), (ts, 7, 1), … in parallel — three fresh generators called with next_id(1000) return the identical ID 4194332672, three times. The machine id is the only thing separating two generators, and it is separating nothing here. This is the same failure as §6.1's machine_id=1025 aliasing to 1, arrived at through a deployment mistake rather than an arithmetic one — and it is why a real system allocates machine ids from a service instead of a config file.

Question 5

Why does the persisted high-water mark leave 167 duplicates and not 168, or 240?

Answer

Because the replacement's first ID in millisecond 99 is seq = 1, not seq = 0. It starts with last_ts = 99 (the stored mark) and seq = 0; the first call finds now == last_ts and increments before packing, so it issues seq 1. The old process had used seq 0 through 167 in that millisecond — 8 genuine plus 20 ms of clamped requests at 8/ms — so the replacement replays 1 through 167, which is 167 IDs. Seq 0 is the one it skips. It is not 240 because that is row A, where the replacement believes the rewound clock and replays 30 whole milliseconds instead of one.

Question 6

You keep refuse but wrap it in a retry loop, so callers never see an error. Does the 400−K result go away?

Answer

Yes, and it is replaced by something worse in a different way. With no failures escaping, the probe never fires, so refuse stays at 0 duplicates (the K=0 column) — it becomes clamp with the availability of a brick wall. The 392 refusals become 392 stalled callers: the first refusal is at true t=100.000 and the wall does not read 99 again until t=149.000, so that caller waits 49 ms, and the last one (t=148.875) waits 0.125 ms. The result to take away is not "refusing is bad", it is that a defence built on process state must not signal failure to anything that can restart the process. Making the failure invisible is one way to satisfy that; not failing at all — clamp — is the cheaper way.

Question 7

The free-running sequence produced 0 duplicates on the 200 ms trace, including across a restart. Is it safe?

Answer

No, it is lucky. The counter is rate × t mod 4096; a replacement starts at 0, so the two collide when the restart sits a whole multiple of 4096/rate = 512 ms after the clock step. The 200 ms trace is too short to contain such an offset. Given a 1000 ms trace with a 200 ms rewind at t=600, a restart at t=712 (712 − 200 = 512) reissues wall milliseconds 512–599 in full: 704 duplicates, while restarts at 708 or 716 cost nothing (§6.6). It also destroys within-millisecond ordering and wraps every 512 ms at this rate.

10. Further reading

Every link below was fetched and confirmed live when this was written.

Elsewhere in this repo, distributed-lock is the other toy where correctness rests on a clock nobody controls, and failure-detector is the other one where the mechanism watching for failure is the thing that causes it.