#Unique ID generation
AddedEvery row, message, order and upload needs a name that no other one will ever have. On one database that's easy: the database counts 1, 2, 3… for you. The problem starts when many machines create records at the same time and none of them should have to ask anyone else first. Asking costs a network round trip on every insert, and whatever you ask becomes a single point of failure. So the question an interviewer is really asking with "how do you generate IDs?" is: how do you make names unique without coordination, and what else do you want the name to tell you?
A good mental model is a ticket roll stamped with the time. Give every worker its own roll with its own colour (the machine ID). Each ticket it tears off is stamped with the current millisecond and a running number for that millisecond. Two workers can never print the same ticket, because their colours differ; one worker can never print the same ticket twice, because either the time or the running number has moved on. Sort the tickets and you get them back roughly in the order they were printed. That's Snowflake. A UUID v4 is the opposite approach: no roll, no colour, just a number so large and so random that a repeat is never going to happen in practice.
Here are the choices you'll be expected to compare:
| Approach | Size | Sortable by time | Needs coordination | What it leaks |
|---|---|---|---|---|
| Database auto-increment | 8 bytes (BIGINT) | Yes | Every insert goes to the one database that owns the counter | Volume: ID 1,000,000 says you have a million rows |
| Ticket server (Flickr, 2010) | 8 bytes | Yes | One round trip to a small counter database per ID (or per batch) | Volume |
| UUID v4 | 16 bytes | No | None | Nothing: 122 random bits |
| UUID v7 | 16 bytes | Yes (to the millisecond) | None | Creation time to the millisecond |
| Snowflake | 8 bytes | Yes (to the millisecond) | Only to hand out machine IDs once | Creation time, which machine, how busy it was that millisecond |
The layouts above are the published ones. Twitter announced Snowflake in 2010: one unused sign bit, 41 bits of milliseconds since its own epoch (1288834974657, which is 4 November 2010), 10 bits of machine ID (the original Scala code split it into 5 bits of datacenter and 5 of worker) and a 12-bit sequence. UUID v7 and v4 are defined in RFC 9562 (May 2024, which replaced RFC 4122): v7 is a 48-bit big-endian Unix millisecond timestamp, the version nibble 0111, 12 bits called rand_a, the variant bits 10, and 62 bits called rand_b. The RFC says implementations "SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6 if possible".
Here is a Snowflake generator. The clock is injected, so the run is the same every time and you can freeze time or step it backwards on purpose:
// A Snowflake generator: 1 unused bit | 41 bits ms since epoch | 5 datacenter | 5 worker | 12 sequence.
const EPOCH = 1288834974657n; // Twitter's epoch: 2010-11-04T01:42:54.657Z
class Snowflake {
constructor(datacenter, worker, clock) {
this.dc = BigInt(datacenter);
this.worker = BigInt(worker);
this.clock = clock; // injected so the run is reproducible
this.lastMs = -1n;
this.seq = 0n;
}
next() {
let ms = BigInt(this.clock.now());
if (ms < this.lastMs) throw new Error(`clock moved back ${this.lastMs - ms} ms; refusing to issue IDs`);
if (ms === this.lastMs) {
this.seq = (this.seq + 1n) & 0xfffn; // 12 bits: 0..4095
if (this.seq === 0n) {
// 4096 IDs already issued this millisecond: wait for the next one
while (ms <= this.lastMs) ms = BigInt(this.clock.waitTick());
}
} else this.seq = 0n;
this.lastMs = ms;
return ((ms - EPOCH) << 22n) | (this.dc << 17n) | (this.worker << 12n) | this.seq;
}
}
const decode = (id) => ({
time: new Date(Number((id >> 22n) + EPOCH)).toISOString(),
datacenter: Number((id >> 17n) & 31n),
worker: Number((id >> 12n) & 31n),
sequence: Number(id & 0xfffn),
});
// Fake clock: frozen at one millisecond until someone waits.
let t = Date.UTC(2026, 9, 24, 12, 0, 0, 0);
const clock = { now: () => t, waitTick: () => ++t };
const gen = new Snowflake(3, 17, clock);
const ids = Array.from({ length: 5000 }, () => gen.next());
const show = (label, id) => console.log(label, String(id), JSON.stringify(decode(id)));
show("id 1: ", ids[0]);
show("id 4096:", ids[4095]);
show("id 4097:", ids[4096]);
console.log("all unique:", new Set(ids).size === ids.length);
console.log("sorted by creation:", ids.every((id, i) => i === 0 || id > ids[i - 1]));
console.log("digits:", ids[0].toString().length, "| fits in a signed 64-bit long:", ids[0] < 2n ** 63n);
t -= 5; // NTP steps the clock back 5 ms
try {
gen.next();
} catch (e) {
console.log("after clock step back:", e.message);
}
const yearsOf41Bits = Number(2n ** 41n) / (365.25 * 24 * 3600 * 1000);
console.log("41-bit ms lasts", yearsOf41Bits.toFixed(1), "years -> runs out in", new Date(Number(EPOCH) + 2 ** 41).getUTCFullYear());id 1: 2113963715589509120 {"time":"2026-10-24T12:00:00.000Z","datacenter":3,"worker":17,"sequence":0}
id 4096: 2113963715589513215 {"time":"2026-10-24T12:00:00.000Z","datacenter":3,"worker":17,"sequence":4095}
id 4097: 2113963715593703424 {"time":"2026-10-24T12:00:00.001Z","datacenter":3,"worker":17,"sequence":0}
all unique: true
sorted by creation: true
digits: 19 | fits in a signed 64-bit long: true
after clock step back: clock moved back 5 ms; refusing to issue IDs
41-bit ms lasts 69.7 years -> runs out in 2080- The first call reads the fake clock:
2026-10-24T12:00:00.000Z, which is1792843200000ms since 1970. Subtracting Twitter's epoch leaves504008225343ms; shifting that left by 22 bits puts it in the top 41 bits, and the datacenter (3 << 17) and worker (17 << 12) are ORed in below it. The sequence is0, so the first ID is2113963715589509120. - The clock is frozen, so calls 2 to 4096 all see the same millisecond. Each one takes the
ms === this.lastMsbranch and bumpsseq:0 → 1 → … → 4095. The IDs differ only in their last 12 bits, which is why ID 4096 is exactly4095more than ID 1 (…509120 → …513215). - Call 4097 would need sequence 4096, which doesn't fit in 12 bits:
(4095 + 1) & 0xfffwraps to0. Reusing0in the same millisecond would duplicate ID 1, so the generator waits for the next millisecond (waitTick) and issues…593703424stamped.001. That wait is the throughput limit: one worker can issue 4,096 IDs per millisecond, about 4.1 million per second, and then it stalls. - All 5,000 IDs are unique and strictly increasing, because each one is "later time, or same time and bigger sequence". That's the property that makes Snowflakes good primary keys: new rows always land at the right-hand end of the index.
- Then the clock steps back 5 ms, as it can when NTP corrects a fast clock. If the generator carried on, it would re-enter milliseconds it already used and could repeat an ID it has already handed out. This version refuses and throws; the alternatives are below.
- 41 bits of milliseconds is
2^41 / (365.25 × 86,400,000)= 69.7 years, so Twitter's IDs run out in 2080. A custom epoch exists precisely to spend those 69 years from your launch date rather than from 1970 (counting from 1970, a 41-bit timestamp would already have run out in 2039).
This is the question interviewers probe once you've drawn the bit layout, because it's where Snowflake depends on something it can't control. Wall clocks on servers are corrected by NTP, and a correction can step the clock backwards (see Why distributed systems are hard). Your options:
- Refuse until the clock catches up (what the model does, and what Twitter's original did: it threw an exception). Fine for a step of a few milliseconds; a step of a minute means a minute of errors from that worker.
- Wait it out if the step is small: sleep
lastMs - nowmilliseconds and carry on. Cap the wait (say 10 ms) and refuse beyond that. - Keep your own monotonic clock: never let
msgo belowlastMs; if the wall clock is behind, keep issuing fromlastMswith the sequence, borrowing from the future. IDs stay unique and ordered; the timestamp inside them is slightly wrong for a while. This is the same idea as a hybrid logical clock. - Run the clock with slewing, not stepping: configure NTP or chrony to slew (speed up or slow down gradually) so time never goes backwards on a running machine. This removes most of the problem at the source.
UUIDs need no machine IDs at all. A v4 is 122 random bits, and the chance of two ever colliding is negligible (the birthday maths below shows why: with 122 bits, you'd need around 2.7 × 10^18 of them for even a 50% chance of a single repeat). The cost is that they are 16 bytes and completely unordered, and that matters for databases. A v7 keeps 74 random bits for uniqueness and spends the first 48 on the time, so it sorts by creation time like a Snowflake while needing no coordination like a v4.
The model builds both by hand from the RFC layout (with a seeded random source so the output is reproducible), then checks Node's own generators:
import { randomUUID, randomUUIDv7 } from "node:crypto";
// Seeded PRNG (mulberry32) so the "random" bytes are the same on every run.
const mulberry32 = (a) => () => {
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 2 ** 32;
};
const rand = mulberry32(42);
const randomBytes = (n) => Uint8Array.from({ length: n }, () => Math.floor(rand() * 256));
const hex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
const format = (b) => hex(b).replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, "$1-$2-$3-$4-$5");
// RFC 9562, section 5.7: 48-bit unix_ts_ms | ver=0111 | 12-bit rand_a | var=10 | 62-bit rand_b
function uuidv7(ms) {
const b = randomBytes(16);
for (let i = 0; i < 6; i++) b[i] = Number((BigInt(ms) >> BigInt(8 * (5 - i))) & 0xffn);
b[6] = (b[6] & 0x0f) | 0x70; // version 7 in the top 4 bits of octet 6
b[8] = (b[8] & 0x3f) | 0x80; // variant 10 in the top 2 bits of octet 8
return format(b);
}
// RFC 9562, section 5.4: 122 random bits, then the same version (0100) and variant bits
function uuidv4() {
const b = randomBytes(16);
b[6] = (b[6] & 0x0f) | 0x40;
b[8] = (b[8] & 0x3f) | 0x80;
return format(b);
}
const start = Date.UTC(2026, 9, 24, 12, 0, 0);
const v7 = [0, 1, 1, 2, 5].map((dt) => uuidv7(start + dt)); // created in this order
const v4 = Array.from({ length: 5 }, uuidv4);
console.log("v7, in creation order:");
v7.forEach((u) => console.log(" ", u));
console.log("v7 sorted as strings == creation order?", JSON.stringify([...v7].sort()) === JSON.stringify(v7));
console.log("v4, in creation order:");
v4.forEach((u) => console.log(" ", u));
console.log("v4 sorted as strings == creation order?", JSON.stringify([...v4].sort()) === JSON.stringify(v4));
const ms = parseInt(v7[0].replace(/-/g, "").slice(0, 12), 16); // the first 48 bits
console.log("timestamp read back from v7[0]:", new Date(ms).toISOString());
console.log("version nibble:", v7[0][14], "| variant bits of", v7[0][19], "=", parseInt(v7[0][19], 16).toString(2).slice(0, 2));
// 100 IDs inside ONE millisecond: the timestamp ties, so order falls to the random bits.
const burst = Array.from({ length: 100 }, () => uuidv7(start + 9));
const outOfOrder = burst.filter((u, i) => i > 0 && u < burst[i - 1]).length;
console.log("same-millisecond burst: out of order", outOfOrder, "of 99 neighbours");
// Node's own generators (randomUUIDv7 needs Node 26.1+ or 24.16+): only print what is stable between runs.
const a = randomUUIDv7(), b = randomUUID();
console.log("node randomUUIDv7 version:", a[14], "| node randomUUID version:", b[14]);
console.log("random bits: v4 =", 128 - 4 - 2, "| v7 =", 128 - 48 - 4 - 2);v7, in creation order:
01a16dc8-3600-759f-9d78-3fe1be4e3280
01a16dc8-3601-7708-840f-2fc887062cd7
01a16dc8-3601-7e98-bea5-354dbddb8234
01a16dc8-3602-7d16-b16e-f1231c045c7b
01a16dc8-3605-742d-85d2-1efe90ea6f94
v7 sorted as strings == creation order? true
v4, in creation order:
569852ba-4b73-47b1-bee3-6e8b4b19b250
c9e71779-d221-492d-b5b3-396929a75534
a0e353ca-6421-4d5e-82bc-a2e0e8359a7d
2f0cb4b5-4a46-4607-b8fe-92fd292b8f4a
656f6baf-6656-4553-be9c-8b963c534c52
v4 sorted as strings == creation order? false
timestamp read back from v7[0]: 2026-10-24T12:00:00.000Z
version nibble: 7 | variant bits of 9 = 10
same-millisecond burst: out of order 45 of 99 neighbours
node randomUUIDv7 version: 7 | node randomUUID version: 4
random bits: v4 = 122 | v7 = 74uuidv7(ms)starts from 16 random bytes, then overwrites bytes 0–5 with the timestamp, most significant byte first.1792843200000in hex is01a16dc83600, which is exactly the first 12 hex digits of the first v7 (01a16dc8-3600). Byte 6 gets0111in its top nibble, so the 13th hex digit is always7; byte 8 gets10in its top two bits, so the 17th hex digit is always8,9,aorb.- The five v7s were made 0, 1, 1, 2 and 5 ms after the start, and their prefixes go
…3600,…3601,…3601,…3602,…3605. Because the time sits in the most significant bits and the hex is fixed-width, plain string sorting gives creation order:true. The v4s have no time in them, so sorting scrambles them:false. - The timestamp can be read back from the first 48 bits (
2026-10-24T12:00:00.000Z). That's a feature for debugging and for range queries ("rows created today" is a key range), and a leak if creation time is sensitive. - The two IDs made in the same millisecond (
…3601-7708and…3601-7e98) happen to be in order, but that was luck: in the burst of 100 IDs in one millisecond, 45 of 99 neighbours are out of order, as you'd expect from coin flips. Within a millisecond, a plain v7 is only as ordered as its random bits. - Node 26 has both generators built in:
crypto.randomUUID()gives version4andcrypto.randomUUIDv7()gives version7(the release notes list it in Node 26.1.0 on 7 May 2026, and backported to the 24.x LTS line in 24.16.0 on 21 May 2026). The last line is the trade: v7 gives up 48 random bits (122 → 74) to buy time ordering; 74 random bits per millisecond is still far more than enough to avoid collisions.
RFC 9562 (section 6.2) knows about that same-millisecond disorder and offers three fixes: a counter in rand_a that increments within the millisecond (method 1), treating the random bits as a counter that grows by a random step (method 2), or putting a sub-millisecond clock fraction in the 12 bits of rand_a (method 3). Implementations differ, so check yours:
- PostgreSQL 18 (released 25 September 2025) added
uuidv7(). Its source uses method 3, with 12 bits of sub-millisecond fraction, and guarantees each value is greater than the previous one within the same backend (connection). Runninguuidv7()100,000 times in one query on PostgreSQL 18.6 gave 0 out-of-order neighbours. - Node's
crypto.randomUUIDv7()is not monotonic within a millisecond. 200,000 calls in a tight loop on Node 26.7 gave 100,138 out-of-order neighbours (about half, as with the model's burst).
That matters less than it sounds. Within one millisecond the order is meaningless anyway, and B-tree locality only needs IDs to be roughly increasing. It matters if you use the ID itself as a cursor or a "happened before" signal. Then you need a monotonic generator, and even that only orders IDs from one generator, never across machines.
This is the strongest argument for v7 or Snowflake over v4, so it's worth seeing in numbers. A B-tree index keeps keys sorted in fixed-size pages. A new time-ordered key always goes into the rightmost page, which is hot in memory. A random key goes to a random page, which is probably not in memory, and that page is often full, so it splits. The model inserts 200,000 keys into the leaf level of a B-tree (100 keys per page) with a 64-page buffer cache, and counts page reads and how full the pages end up:
// Insert 200,000 keys into a B-tree's leaf level (100 keys per page) with a 64-page buffer cache.
// Compare random keys (UUID v4) with time-ordered keys (UUID v7, Snowflake, auto-increment).
const mulberry32 = (a) => () => {
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 2 ** 32;
};
const PAGE = 100, CACHE = 64, N = 200_000;
function run(nextKey) {
const pages = [[]]; // leaf pages in key order; each holds sorted keys
const cache = new Map(); // LRU of page objects
let misses = 0;
const touch = (p) => {
if (cache.has(p)) cache.delete(p); else misses++; // a miss = a page read from disk
cache.set(p, true);
if (cache.size > CACHE) cache.delete(cache.keys().next().value);
};
for (let i = 0; i < N; i++) {
const key = nextKey(i);
let lo = 0, hi = pages.length - 1; // last page whose first key <= key
while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (pages[mid][0] <= key) lo = mid; else hi = mid - 1; }
const page = pages[lo];
touch(page);
let j = page.length; while (j > 0 && page[j - 1] > key) j--;
page.splice(j, 0, key);
if (page.length > PAGE) {
// Appending at the far right: start a fresh page and leave this one full (what B-trees do).
// Anywhere else: split the page in half.
const rightmostAppend = lo === pages.length - 1 && j === page.length - 1;
const cut = rightmostAppend ? PAGE : PAGE / 2;
pages.splice(lo + 1, 0, page.splice(cut));
}
}
const fill = N / (pages.length * PAGE);
return `${pages.length} pages, ${(fill * 100).toFixed(0)}% full, ${misses} page reads (${((misses / N) * 100).toFixed(1)}% of inserts)`;
}
const rand = mulberry32(7);
console.log("random (v4) :", run(() => rand()));
console.log("time-ordered (v7) :", run((i) => i + rand() * 0.5)); // increasing, a little jitterrandom (v4) : 2880 pages, 69% full, 178014 page reads (89.0% of inserts)
time-ordered (v7) : 2000 pages, 100% full, 2000 page reads (1.0% of inserts)- Each insert finds its leaf page by binary search on the pages' first keys, "reads" the page through the LRU cache (a miss counts as a disk read), and inserts the key in sorted position. A page that overflows 100 keys splits.
- With random keys, the target page is anywhere among thousands, and only 64 fit in the cache, so 89% of inserts need a page read: 178,014 reads for 200,000 inserts. On a real database each of those is a random I/O, plus write-ahead log and checkpoint work for every page dirtied.
- Random inserts also split pages in the middle, leaving two half-full pages that fill up slowly. The index ends at 69% full (2,880 pages), the classic figure for random B-tree inserts: about 44% more pages, and more memory to cache the same data.
- With increasing keys, every insert lands on the rightmost page. When it fills, a B-tree starts a new page and leaves the old one full instead of splitting it in half. So there are exactly 2,000 page reads (one per new page), and 100% fill with 2,000 pages. The model simplifies here: PostgreSQL's B-trees leave 10% free on that rightmost split (the default leaf
fillfactoris 90), so a real index ends nearer 90% full. - This is why "UUID v4 primary keys are slow" is a real complaint once the index outgrows memory, and why v7 fixes it without giving up coordination-free generation.
Size matters too, especially on MySQL, where the MySQL 8.4 manual says "each record in a secondary index contains the primary key columns for the row". An estimate of the key bytes alone:
// What the choice of key type costs in index space, before page overhead.
// Assumptions: 1 billion rows; the primary key appears in the primary index and is copied into
// each of 3 secondary indexes (InnoDB secondary indexes store the primary key) and into 2 tables that reference it.
const rows = 1e9, copies = 1 + 3 + 2;
const types = { "BIGINT (Snowflake, auto-increment)": 8, "UUID as BINARY(16) / Postgres uuid": 16, "UUID as CHAR(36) text": 36 };
for (const [name, bytes] of Object.entries(types)) {
const gib = (rows * copies * bytes) / 2 ** 30;
console.log(`${name.padEnd(36)} ${String(bytes).padStart(2)} B x ${copies} copies -> ${gib.toFixed(0).padStart(4)} GiB of key bytes`);
}BIGINT (Snowflake, auto-increment) 8 B x 6 copies -> 45 GiB of key bytes
UUID as BINARY(16) / Postgres uuid 16 B x 6 copies -> 89 GiB of key bytes
UUID as CHAR(36) text 36 B x 6 copies -> 201 GiB of key bytes- The assumptions are in the comment: a billion rows, and the key stored six times (primary index, three secondary indexes that carry the primary key, two foreign keys elsewhere).
- A 64-bit key costs 45 GiB across those copies; a binary UUID doubles it to 89 GiB. That's a real difference, but rarely the deciding one on its own.
- Storing UUIDs as 36-character text costs 201 GiB, 4.5 times the
BIGINT. That's a mistake, not a trade-off: store them as PostgreSQL's nativeuuidtype or MySQLBINARY(16).
// A 64-bit ID sent as a JSON number loses digits in JavaScript: Number has 53 bits of integer precision.
const body = '{"id": 2113963715589509121, "id_str": "2113963715589509121"}';
const parsed = JSON.parse(body);
console.log("as number:", parsed.id, "| exact?", String(parsed.id) === parsed.id_str);
console.log("as string:", parsed.id_str, "-> BigInt", BigInt(parsed.id_str));
console.log("largest safe integer:", Number.MAX_SAFE_INTEGER, `(2^53 - 1, ${String(Number.MAX_SAFE_INTEGER).length} digits)`);
console.log("neighbours collapse:", 2113963715589509121 === 2113963715589509120);
// A recent JavaScript addition (TC39 "JSON.parse source text access"): the reviver sees the raw text.
const exact = JSON.parse(body, (key, value, ctx) => (key === "id" ? BigInt(ctx.source) : value));
console.log("reviver with source:", exact.id);as number: 2113963715589509000 | exact? false
as string: 2113963715589509121 -> BigInt 2113963715589509121n
largest safe integer: 9007199254740991 (2^53 - 1, 16 digits)
neighbours collapse: true
reviver with source: 2113963715589509121nJSON.parseturns2113963715589509121into the nearest double, which prints as2113963715589509000. The last digits are gone, and nothing throws.- At this size, doubles are 256 apart, so two different IDs,
…121and…120, become the same number:true. A client could merge two users or fetch the wrong tweet. - Strings survive intact and convert to
BigIntwhen you need arithmetic. The rule for an API: send 64-bit IDs as strings. The newerJSON.parsereviver context (ctx.source, which works on the Node 26 used here) can rescue a number field, but you can't count on every client having it.
Before Snowflake, Flickr's 2010 post "Ticket Servers: Distributed Unique Primary Keys on the Cheap" described the simplest coordinated scheme: a tiny MySQL database whose only job is a table with one row and an auto-increment column. Each request does REPLACE INTO Tickets64 (stub) VALUES ('a') and reads LAST_INSERT_ID(). For availability they ran two ticket servers, one issuing odd numbers and one even (write-ups describe auto-increment-increment = 2 with offsets 1 and 2). It works, it's 64 bits and roughly ordered, but every ID costs a round trip, and you have a database to run just for counting. The common improvement is to fetch a block (+1000 at a time) and hand IDs out from memory: one round trip per thousand IDs, at the cost of gaps when a server restarts with an unused block.
Instagram's scheme (from its engineering post "Sharding & IDs at Instagram") is a third variant worth knowing because it reuses the database you already have: 41 bits of milliseconds, 13 bits of logical shard ID and 10 bits of a per-shard sequence (mod 1024), generated by a PL/pgSQL function inside each PostgreSQL shard. The shard ID inside the ID means you can find a row's shard from its ID alone. Discord's IDs are Snowflakes too, with its own epoch (the first second of 2015).
With the standard layout (10 bits of machine ID, 12 bits of sequence), what's the maximum rate for one worker, and for the whole deployment?
Show answer
One worker: 4,096,000 per second. The whole deployment: about 4.19 billion per second.
- The sequence has 12 bits, so one worker can issue
2^12 = 4,096IDs in one millisecond before it must wait for the clock to tick. - There are 1,000 milliseconds in a second, so one worker tops out at
4,096 × 1,000 = 4,096,000IDs per second. - The machine field has 10 bits, so at most
2^10 = 1,024workers can run at once without sharing a number. 4,096,000 × 1,024 = 4,194,304,000IDs per second in total. You'd run out of CPU long before that; the real limits are per-worker bursts and the number of worker IDs. If you need more workers, take bits from the sequence (fewer IDs per millisecond each), and if you need more years, take bits from the machine field. The layout is a budget you can re-split.