Design a distributed unique ID generator
Reviewed
Mint 64-bit Snowflake IDs: 41 bits of milliseconds since a custom epoch, 10 bits of machine id, 12 bits of per-millisecond sequence. That gives unique, roughly time-ordered IDs at about 100k per second, minted by hundreds of processes with no coordination per ID. Coordination happens once per process start, when the process leases its machine id, plus a small lease renewal every 10 seconds.
Adapted from the HLD + LLD Interview Handbook's Unique ID Generator case studyClarifying questions
Ask before you draw anything. The answers pick the winning option.
- 01What creates IDs: posts, chat messages, orders, uploads, events? How many per second at peak?
- 02Unique across the whole platform, or only inside one service?
- 03Ordered by time? Strictly, or roughly?
- 04Must they fit in 64 bits, or are 128 bits fine?
- 05Who reads an ID back, and do they need its timestamp or minting machine?
- 06Must IDs be unguessable? Can a user enumerate them?
- 07How many minting processes? One datacenter or several?
- 08Is there a database already? Is sharding planned?
Requirements
Functional: every request gets a unique ID with no duplicates. IDs are roughly time-ordered, so a later ID usually sorts after an earlier one. Any ID can be split back into timestamp, machine, and sequence. Time windows map to ID ranges for scans.
Non-functional: about 100k IDs per second at peak. Minting stays well under 1 ms with no network call per ID. One dead worker or a short registry outage does not stop minting. Hundreds of processes across datacenters. Keys stay at 8 bytes.
Out of scope: strict global order across machines, which independent clocks cannot provide. Short human-readable codes, which belong to the URL shortener. Unguessable client-facing IDs, which call for the 128-bit schemes covered below.
Estimation
Two numbers shape the design. Under this case study's database-writer assumptions, 100k/s exceeds one writer's budget, yet leaves substantial headroom under the theoretical per-worker bit budget. Actual database and generator throughput must be measured. Key size adds up: assuming 35k IDs/s all year, 8-byte keys come to roughly 8.8 TB per year and 16-byte UUIDs to roughly 17.7 TB, before indexes and replication.
Ascending keys append to the right edge of a B-tree; random keys land everywhere and split pages. Time order is a storage win, not just a sorting convenience.
| Quantity | Number | Derivation |
|---|---|---|
| Mint rate, peak | ~100k/s | Posts ~5k/s + chat ~70k/s + orders, uploads, events |
| Mint rate, average | ~35k/s | Peak is roughly 3x average |
| Per-worker ceiling | 4.096M/s theoretical | 4,096 per ms x 1,000 ms; a bit budget, not a Python benchmark. Real throughput is finite and hardware-dependent. |
| Read QPS (decompose, admin) | Well under 1k/s | Minting dominates |
| Bandwidth (service form only) | ~5 MB/s = 40 Mbps | Only the service form has network traffic |
| Ticket-range buffer | 8 MB total | Ranges held in memory across workers |
| Coordination load | ~100 tiny writes/s | 1,024 workers x 1 renewal per 10 s |
| Key bytes per year at 35k/s | ~8.8 TB (8 B) vs ~17.7 TB (16 B) | Sizing assumption: 35,000 x 31.5M s x key bytes |
Good: UUIDv4 or database auto-increment
UUIDv4 is 128 bits with 122 of them random. No coordination, no single point of failure, and minting is not limited by any shared component. The cost: 16-byte keys, page splits from random inserts, no time order (latest-first needs a created_at index), and IDs that tell you nothing in logs.
Auto-increment lets one database hand out the next integer: 64-bit and increasing in allocation order, but not necessarily transaction commit order. One leader limits throughput, illustratively around 5k to 20k writes per second depending on hardware and configuration. Availability needs replication and failover; unsafe counter recovery can re-issue values, while rollbacks and range reservations can leave gaps.
Good is enough with one database, low volume, and no sharding plans: choose auto-increment. If 128 bits are fine and order does not matter, choose UUIDv4.
Better: a ticket server with pre-fetched ranges
The Flickr ticket-server idea: a worker requests a block of 1,000 IDs, hands them out locally, and asks again when the block runs dry. One round trip per block instead of per ID. Two databases with odd and even offsets keep it available if one fails. IDs are 64-bit and roughly ordered per server.
The cost: every block request hits one hot row. Blocks lost on restart leave harmless gaps. There is still a round trip per block, and the ceiling stays below Snowflake.
Better is enough below about 10k IDs per second, without a sub-millisecond latency need, and before multiple databases arrive.
Best: Snowflake with a 41 / 10 / 12 bit split
This answers the stated ask. Twitter published Snowflake in 2010. An ID fits in a signed 64-bit integer: one zero sign bit plus a 63-bit payload, so it stays non-negative in databases and Java longs.
The split gives 4,096 IDs per millisecond per worker, a theoretical 4.096M per second. 41 bits of milliseconds last about 69 years from a 2024 epoch, into the 2090s. 10 machine bits allow 1,024 workers.
The split is a budget. Instagram chose 41 bits of time, 13 of shard, and 10 of sequence, minted per shard inside Postgres by a PL/pgSQL function: 1,024 IDs per millisecond per shard, and the shard in the key routes reads with no lookup table.
Composing is two shifts and an OR; decomposing is two shifts and two masks. Run it below. JavaScript numbers lose precision past 2^53, so signed 64-bit IDs are not exact as JSON numbers: send them as strings.
Write path: lease a machine id once at start-up, then mint in memory with a clock read and a few integer operations, no network call. In the same millisecond, increment the sequence; in a new one, reset it to zero. Read path: decompose by shift and mask, and turn a time window into an ID range so the query is a range scan.
Trade-offs: no per-ID coordination, but clock steps and machine-id reuse can both cause duplicates without the guards below. Alert at 80% of the timestamp range. The sequence leaks minting rate, so hide raw IDs where that matters.
| Field | Bits | Meaning |
|---|---|---|
| Sign | 1 | Always 0, keeps the value non-negative |
| Timestamp | 41 | Milliseconds since a custom epoch |
| Machine id | 10 | Minting worker, 0 to 1,023 |
| Sequence | 12 | Counter within one millisecond, 0 to 4,095 |
| Total | 64 | 63-bit payload plus zero sign bit |
Running code requires JavaScript.
Options compared
Follow the decision flow in order.
- 01Must IDs fit in 64 bits? No: need time order? No means UUIDv4; yes means UUIDv7, ULID, or KSUID.
- 0264 bits, one database, no sharding planned: auto-increment.
- 03Otherwise, under ~10k IDs/s and no sub-millisecond need: ticket server with ranges on two databases.
- 04Otherwise: Snowflake.
| Option | Size | Order | Coordination | Throughput | Main weakness |
|---|---|---|---|---|---|
| UUIDv4 | 128-bit, 122 random | None | None | No shared bottleneck | 16-byte keys, random inserts, unreadable |
| DB auto-increment | 64-bit | Allocation order, not commit order | Single leader | Illustratively ~5k-20k writes/s | Leader bottleneck; safe failover required; gaps possible |
| Ticket server (Flickr) | 64-bit | Rough, per server | One round trip per range | Ranges of 1,000 lift it | Hot row; restart gaps |
| Snowflake | 64-bit | K-sortable to the ms | None per ID | 4.096M/s per worker theoretical | Clock steps, machine-id reuse, ~69-year life |
| UUIDv7 / ULID / KSUID | 128-160-bit | ms or s | None | No shared bottleneck | 2x to 2.5x key bytes |
Deep dive: backwards clocks and the drift guard
A naive generator resets its sequence whenever the clock reading changes. After a backwards step from an NTP correction, VM live migration, or leap-second smear, it reuses old milliseconds and mints duplicates.
The fix is a logical millisecond the wall clock can only push forward. Inside one logical millisecond the sequence increments. On overflow the generator borrows the next millisecond instead of spinning. If the logical clock gets more than max_drift_ms ahead of the wall clock, it raises ClockDriftError. Small steps are absorbed; large ones become incidents.
The drift guard only bounds how far the logical clock runs into the future. Uniqueness still depends on exclusive machine-id ownership and restart discipline. Skew between different machines only blurs cross-worker order, because their machine bits differ.
Restarts are the trap. Persisting the last timestamp once a second is not safe: a crash can lose up to a second of minted IDs, and a restarted process with a slow clock would repeat them. Instead, durably reserve a high-water mark ahead of use (for example, write now plus a reservation window before minting inside it) and refuse to start until the wall clock passes it. Or start under a fresh machine id and quarantine the old one until the wall clock is past its previous logical maximum. Twitter also compared clocks with peers at start-up and refused to join when too far off.
Deep dive: machine-id leases and fencing
Two workers sharing a machine id mint duplicates once their clocks line up. The lease prevents that; the primary-key constraint detects it, so alert on violations.
Registration claims the lowest free id as an ephemeral node in ZooKeeper or etcd. The lease is 30 seconds and is renewed every 10 seconds. A lost race retries the next free id. When a session expires, the node is deleted and the id can be reused.
Fencing rule: a worker that cannot renew must stop minting before its lease expires. Check a conservative monotonic deadline on every mint, account for whether the platform clock advances during suspension, and refuse on uncertain ownership after wake-up. Lease expiry alone cannot stop a paused process that later resumes. Where protected writes use fencing tokens, the storage resource must reject stale tokens; checking a token only at the registry cannot fence an offline minter. Safe machine-id reuse also needs a durable timestamp high-water mark or quarantine beyond the previous holder's maximum logical time, including borrowed future milliseconds.
Coordination stays cheap: one registration per process start plus a renewal every 10 seconds, about 100 small writes per second at 1,024 workers. Minting never touches the registry.
| Method | Upside | Downside |
|---|---|---|
| Static config | No dependency | Scaled or recreated fleets reuse ordinals |
| IP-derived low 10 bits | No dependency | Subnet collisions; NAT and IPv6 guesswork |
| ZooKeeper / etcd lease | One holder by construction; reclaimed on crash | Start-up dependency; needs fencing on lease loss |
| DB row | Familiar | A crashed worker keeps its row without a reaper |
Deep dive: sequence overflow
4,096 IDs per millisecond is a theoretical 4.096M per second from one worker. Hitting it means a benchmark, or one process minting for the whole platform.
Treat saturation as a capacity signal and add workers. Do not move bits from machine to sequence: that shrinks the fleet to fix a problem more workers already solve. On overflow, borrow the next millisecond under the drift guard rather than spinning.
Deep dive: ULID, KSUID, and UUIDv7
With 128 bits or more allowed, these keep time order with no infrastructure. High bits are time, so B-tree locality survives. The cost is bigger keys: 16-byte schemes double an 8-byte key (roughly 8.8 TB more per year at the 35k/s sizing assumption), and KSUID's 160 bits are 20 bytes, 2.5x. None orders across machines beyond clock skew.
The timestamp in ULID and KSUID is readable, not secret. Their unguessability comes only from the random part, which must use a cryptographically strong generator.
Pick Snowflake for 8-byte keys on a fleet you control. Pick UUIDv7 when your database and drivers support it. Pick ULID or KSUID when clients create IDs that must be hard to guess.
| Scheme | Bits | Time | Random bits | Text form | Notes |
|---|---|---|---|---|---|
| Snowflake | 64 | 41 bits, ms | None | 18-19 digits | Needs machine ids; sequence reveals rate |
| UUIDv7 (RFC 9562) | 128 | 48 bits, ms | 74 | 36-char UUID | Standard; native in recent databases and languages |
| ULID | 128 | 48 bits, ms | 80 | 26-char Crockford base32 | Sorts as text; monotonic variant increments within a ms |
| KSUID | 160 (20 bytes) | 32 bits, seconds | 128 | 27-char base62 | Second resolution; generate anywhere |
Scaling and failure modes
Return IDs as JSON strings. Cross-datacenter order is approximate; splitting machine bits per datacenter keeps uniqueness intact.
| Failure | What happens | Mitigation |
|---|---|---|
| Hot worker saturates 4,096/ms | Borrowing fills the drift budget and ClockDriftError fires | Spread minting across more workers |
| Registry outage | Workers keep minting only while their 30 s lease (renewed every 10 s) is valid, then stop; new workers cannot register | Run one registry ensemble per datacenter. Do not fall back to database ranges in the same ID namespace. |
| Clock step on one worker | Absorbed up to max_drift_ms; beyond it the worker refuses and is drained | Monitor borrowed_ms and the wall-vs-logical gap |
| Hot partition | Time-sorted keys concentrate writes in range-partitioned stores | Hash the ID to pick a partition; keep time order inside it |
| Cross-datacenter order | Approximate, within clock skew | Split machine bits and registries per datacenter; true order needs one sequencer |
| Lifetime | Timestamp runs out after ~69 years | Alert at 80% of the range |
Trade-offs summary
Each choice and what the alternative would cost.
| Decision | Chosen | Alternative | Cost of alternative |
|---|---|---|---|
| ID width | 64-bit Snowflake | 128-bit UUIDv7 / ULID, 160-bit KSUID | 2x (or 2.5x) key bytes |
| Bit split | 41 / 10 / 12 | 41 / 13 / 10 (Instagram) | 1,024 IDs per ms per shard; shard id routes reads |
| Time resolution | Milliseconds | Seconds | Coarser order, fewer IDs per unit |
| Overflow | Borrow next ms | Spin and wait | Wasted latency under saturation |
| Backwards clock | Logical clock + max_drift_ms | Throw immediately / sleep the skew | Lost availability or stalled minting |
| Machine id | ZooKeeper / etcd lease | Static config / IP-derived | Reused ordinals or collisions cause duplicates |
| Delivery | Embedded library first | Thin service second | Network hop vs a lease per process |
| Public exposure | JSON strings | JSON numbers | Precision loss past 2^53 in JavaScript |
Interviewer follow-ups
Why not UUIDv4 everywhere? 16-byte keys, page splits, and no time order. UUIDv7 fixes the last two if 128 bits are acceptable.
Two workers share a machine id? Duplicates once clocks agree. The lease with fencing prevents it; the primary key detects it.
More than 4,096 IDs in one millisecond? Add workers. Taking machine bits shrinks the fleet.
Strict order across machines? Not with independent clocks. Snowflake is k-sortable within skew; strict order needs one sequencer per stream.
How is Instagram different? 41 / 13 / 10 minted per shard in Postgres, so the shard in the key routes reads.
Two datacenters? Split machine bits and run a registry per datacenter. Uniqueness holds; order is approximate.
Is a mint lock slow? In real CPython the mint path takes a threading.Lock around the clock read and shifts. It is short, but its cost depends on the runtime and contention, so measure rather than assume.
Try it live
Build the generator yourself in the Snowflake ID generator problem: compose and decompose IDs in the browser compiler, with the clock guard included.
The URL Shortener problem also needs unique codes; this guide is its distributed version. For the round itself, read what to expect in a low-level design round.
45-minute pacing
A schedule that leaves time for deep dives.
| Time | What to do |
|---|---|
| 0-5 min | Clarify sources, rate, 64-bit fit, ordering |
| 5-9 min | Estimate mint rate, per-worker ceiling, key bytes per year |
| 9-15 min | Options table: UUIDv4, auto-increment, ticket server, Snowflake, 128-bit schemes |
| 15-24 min | Snowflake write and read paths |
| 24-38 min | Deep dives: clocks, leases, overflow, 128-bit schemes |
| 38-45 min | Failure modes and trade-offs |
Sources and related discussion
- HLD + LLD Interview Handbook: Unique ID Generator
- Twitter Engineering: Announcing Snowflake (2010)
- RFC 9562: UUIDs, including UUIDv7
- Instagram Engineering: Sharding & IDs at Instagram
