System Design · step by step

Design an ID Generator

Step 1 / 9
The numbers to beat41 bitstimestamp~69 yrsof msk-sortedby time

In the interview room

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

Functional requirements

What it must do — agree on these before drawing a single box.

  • Mint an ID: hand back a unique 64-bit integer, locally, millions of times a second.
  • No collisions: two machines minting in the same millisecond never produce the same id.
  • Time-sortable: ids minted later are numerically larger, so they roughly sort by creation order.
  • Burst within a millisecond: a per-ms sequence lets one node mint thousands of ids before waiting for the clock.
  • Assign machine ids: give each generator a unique node id once at startup.

Non-functional requirements

The qualities that shape the whole design — each one names the mechanism that buys it.

No network hop per id
The generator runs as a library inside each service and produces a 64-bit id in-process, so there’s no round-trip on the hot path.
Roughly time-ordered, index-friendly ids
A millisecond timestamp in the top bits makes later ids numerically larger, so they’re k-sorted and new inserts append to the index tail.
Same-millisecond ids never collide, coordination-free
Each generator owns a unique machine-id slice of every millisecond, so two nodes physically can’t produce the same number — no per-id coordination.
Many ids within one millisecond
A per-millisecond sequence counter in the low bits lets one node mint 4,096 ids/ms, only stalling to the next tick on overflow.
No two generators share a machine id
An ID registry leases each generator a free machine id once at startup (ZooKeeper/etcd), the only coordination in the whole system.
No duplicates when the clock rewinds
A clock guard tracks the last timestamp used and refuses to mint if the wall clock steps backward, until time passes the last value.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

A local, in-process 64-bit generatorover a database auto-increment column

One counter is a write bottleneck and single point of failure the whole fleet queues behind, and a central id service adds a network hop to every insert. Minting locally has zero per-id coordination — speed is the point.

A millisecond timestamp in the high bitsover random 128-bit UUIDv4s

Random ids are unordered, so inserts scatter across the DB index (poor locality, page splits, no range scans) and cost twice the size. Time-leading bits make ids k-sorted so new inserts append to the index tail.

A per-generator machine-id fieldover coordinating between machines on each id

Per-id coordination re-introduces the network hop you just removed, and random low bits gamble on birthday-paradox collisions. Disjoint machine slices make ids unique by construction — coordinate once at assignment, not per id.

A registry that leases a machine id at startupover each generator picking a random machine id

Random picks collide across 1,024 slots (birthday paradox), and two colliding generators then clash every millisecond. A lease guarantees uniqueness and handles machines coming and going without accidental reuse.

A clock guard that refuses to mint on rewindover assuming clock rewind is too rare to handle

One NTP correction can reuse a whole millisecond range of ids — the one failure this system must never have. Tracking the last timestamp and waiting out the rewind trades a tiny, rare pause for an ironclad no-duplicates guarantee.

What this teaches

Learn system design by building a distributed unique ID generator like Twitter Snowflake step by step. An interactive guide covering why auto-increment and UUIDs fall short, the timestamp/machine/sequence bit layout, time-sortable 64-bit IDs, machine-id assignment, clock skew, and alternatives.

Key takeaways

  • IDs are generated locally, in-process, with no per-ID network round-trip.
  • A timestamp in the high bits makes 64-bit IDs roughly time-sortable.
  • A machine-id field keeps same-millisecond IDs on different nodes disjoint.
  • A per-millisecond sequence counter handles bursts (4,096 IDs/ms/node).
  • A registry assigns unique machine ids once at startup — the only coordination.
  • A clock guard refuses to mint when the wall clock rewinds, avoiding duplicates.
  • Ticket servers, range allocators and UUIDv7 are alternatives with other trade-offs.

Concepts covered

  • Why is this hard?
  • Local, no round-trip
  • Timestamp in the high bits
  • A machine ID field
  • The sequence counter
  • Assign node bits safely
  • Clock skew & rewind
  • Alternatives & trade-offs

Design a Distributed ID Generator (Snowflake) — read the full walkthrough as text

the same steps, decisions & trade-offs, for reading, reference & search

The big idea

Why is this hard?

“Give me a unique ID” sounds trivial — until it must work across hundreds of machines, millions of times a second, with zero collisions, and ideally produce IDs that sort by time. A single counter can’t keep up, and randomness has its own costs.

Generate IDs locally on each machine with no per-ID coordination, by carving a 64-bit integer into fields — time, machine, sequence — that together guarantee uniqueness. This is Twitter’s Snowflake, and it’s mostly a packing problem.

How to read this: We add one piece at a time, problem then fix, and the diagram grows. Hit Begin.

Step 1 · The naive options

Local, no round-trip

A database auto-increment is one counter — a single point of failure and a write bottleneck the whole fleet must queue behind. Calling a central service per ID adds a network hop to every insert.

Design decision: You need unique IDs millions/sec across hundreds of machines. Where do you generate them?

The call: A local in-process library that mints 64-bit IDs. — Each service generates IDs locally with no network call — speed is the point. The rest is just deciding how to fill 64 bits so they never collide.

Make the generator local: a library inside each service that produces a 64-bit ID in-process, instantly, with no network call. The rest of the design is just deciding how to fill those 64 bits so they never collide.

Why 64 bits: A 64-bit integer is half the size of a UUID, sorts and indexes efficiently in databases, and is plenty of room to encode time, machine and a counter. Smaller IDs mean smaller, faster indexes.

Step 2 · Put time first

Timestamp in the high bits

Random IDs (like a UUIDv4) are unique but unordered — inserting them scatters writes all over a database index, hurting cache locality and range queries. You lose all sense of “newer” vs “older”.

Design decision: You want IDs that don’t scatter DB index writes and that sort by creation time. How?

The call: Put a millisecond timestamp in the high bits. — Later IDs are numerically larger, so they’re roughly time-sortable and new inserts append to the end of the index. 41 bits of ms covers ~69 years from a custom epoch.

Place a millisecond timestamp in the top bits. Now IDs minted later are numerically larger, so they’re roughly time-sortable and new inserts append to the end of the index. 41 bits of milliseconds covers ~69 years from a custom epoch.

k-sorted IDs: IDs aren’t perfectly ordered across machines (clocks differ slightly), but they’re “k-sorted” — close enough to creation order to sort feeds and range-scan by time without a separate timestamp column.

Step 3 · Separate the machines

A machine ID field

Two machines minting an ID in the same millisecond would produce the same timestamp bits — and collide. Coordinating between them on every ID would re-introduce the network hop we just removed.

Design decision: Two machines minting in the same millisecond share timestamp bits and collide. Fix without a network hop?

The call: Give each generator a unique machine-id field. — Same millisecond, different machine bits → different IDs. Each node owns its slice of every millisecond, so they physically can’t collide — coordination happens once at assignment, not per ID. 10 bits = 1,024 generators.

Give each generator a unique machine ID in the middle bits. Same millisecond, different machines, different IDs — uniqueness by construction, with no per-ID coordination. 10 bits allows 1,024 distinct generators.

Partition the ID space: Each machine owns its own slice of every millisecond. Disjoint slices mean two nodes physically cannot produce the same number — coordination happens once (at machine-id assignment), not per ID.

Step 4 · Many IDs per millisecond

The sequence counter

One machine often needs more than one ID per millisecond. Timestamp + machine bits alone would repeat for every ID minted within the same millisecond on the same node.

Design decision: One machine often needs many IDs in the same millisecond. Timestamp + machine bits would repeat. Fix?

The call: A per-millisecond sequence counter in the low bits. — A counter that increments per ID and resets each millisecond lets one machine mint up to 4,096 IDs/ms (12 bits, ~4M/sec). Only if it overflows do you wait for the next tick.

Add a sequence counter in the low bits that increments per ID and resets each millisecond. With 12 bits, a single machine can mint 4,096 IDs per millisecond (~4M/sec). If it overflows, wait for the next millisecond tick.

The full layout: [1 sign | 41 time | 10 machine | 12 seq] = 64 bits. Time keeps it sorted, machine keeps nodes disjoint, sequence handles bursts within a millisecond. That’s the whole trick.

Step 5 · Hand out machine IDs

Assign node bits safely

The whole scheme breaks if two generators are accidentally given the same machine ID — then they collide every millisecond. So who assigns those 1,024 ids, and how do you avoid duplicates as machines come and go?

Design decision: The scheme breaks if two generators get the same machine id. Who assigns the 1,024 ids?

The call: A registry leases a free machine id at startup (ZooKeeper/etcd/config). — Each generator claims a free id once on startup and holds it via a lease. That’s the only coordination in the whole system — after it, every node runs independently and collision-free.

Use an ID Registry: on startup each generator claims a free machine id from a coordination service (ZooKeeper/etcd) or static config, holding it via a lease. The only coordination in the entire system happens here, once per process.

Coordinate once, not per ID: Push all the coordination cost to startup. After a generator has its machine id, it runs fully independently — fast, local, and collision-free for as long as it holds its lease.

Step 6 · When the clock lies

Clock skew & rewind

IDs trust the wall clock — but NTP can step the clock backward. If time moves back, a machine could reuse timestamps it already used and mint duplicate IDs. The one assumption Snowflake makes is the one thing clocks violate.

Design decision: NTP can step the wall clock backward. What stops the generator minting duplicate IDs?

The call: Track the last timestamp; refuse to mint if time goes backward. — A clock guard remembers the last value used and stalls/refuses until the clock passes it — never issuing an ID for a time it already served. A tiny rare pause buys an ironclad no-duplicates guarantee.

A Clock Guard tracks the last timestamp used. If the clock goes backward, the generator refuses to mint (or briefly stalls) until the clock catches up past the last value — never issuing an ID for a time it has already served.

Monotonic, not just current: The generator cares about monotonically increasing time, not absolute time. Detecting rewind and waiting it out trades a tiny, rare pause for an ironclad no-duplicates guarantee.

Step 7 · Other ways

Alternatives & trade-offs

Snowflake gives unique, k-sorted IDs without coordination — but it leaks creation time (a privacy/competitive signal) and isn’t strictly monotonic across machines. Sometimes you need different properties.

Pick the tool for the job: a DB ticket server or range allocator (hand out blocks of ids per machine) gives strict ordering at the cost of a central component; UUIDv7 offers time-ordered randomness with no machine-id assignment. Snowflake wins when you want local, fast, sortable, and 64-bit.

No free lunch: Every scheme trades among coordination, ordering, size and information leakage. Knowing why Snowflake’s fields exist lets you reason about which alternative fits a given workload.

You did it

You just designed an ID generator.

  • IDs are generated locally, in-process, with no per-ID network round-trip.
  • A timestamp in the high bits makes 64-bit IDs roughly time-sortable.
  • A machine-id field keeps same-millisecond IDs on different nodes disjoint.
  • A per-millisecond sequence counter handles bursts (4,096 IDs/ms/node).
  • A registry assigns unique machine ids once at startup — the only coordination.
  • A clock guard refuses to mint when the wall clock rewinds, avoiding duplicates.
  • Ticket servers, range allocators and UUIDv7 are alternatives with other trade-offs.
RUN IT YOURSELF

Snowflake IDs, in Python & TypeScript

Snowflake packs a timestamp, machine id, and sequence into one 64-bit id — unique, sortable, and generated without coordination. Here it is in both languages, running live. Switch tabs, read the comments, and hit Run.

HOW TO READ THE CODE — 4 IDEAS
  1. A 64-bit id is split into fields: 41 bits time | 10 bits machine | 12 bits sequence.
  2. Build an id by shifting each field into place and OR-ing them together (makeId).
  3. Read fields back by shifting + masking (steps 1–3).
  4. Because time is the high bits, ids are roughly time-sortable — and never collide across machines.
CPython · WebAssembly
built to be packed, not memorized — make the calls, rewind the clock, run the gauntlet.
Finished this one? 0 / 62 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.