Skip to content

Unique ID generator

intermediateCommon atTwitter/XDiscordInstagramStripeGoogleRead firstBit manipulationTime handlingDistributed thinkingPractise in the Sandbox →

Every row, message and event needs an id that is unique across the whole fleet, ideally sortable by creation time, ideally 64 bits, and minted without a network call. Random 128-bit UUIDs give uniqueness with no coordination but are big and scatter database inserts. A database counter gives small ordered ids but puts one server on every write. The standard answer is a Snowflake-style id: 41 bits of milliseconds since a custom epoch, 10 bits of worker id, 12 bits of per-millisecond sequence. O(1) and no I/O per id, 4,096 ids per ms per worker, about 70 years of range. The coordination moves to startup (claim a unique worker id), and the risk moves to the clock.

A single database’s AUTO_INCREMENT is the easy answer until you shard: two shards both produce id 1001, and the one database that hands out ids becomes a bottleneck and a single point of failure. Twitter hit this in 2010 when moving tweets off MySQL, and built Snowflake.

What an id is asked to do:

Goal What it means Pulls toward
Unique Never the same id twice, across all machines, forever Coordination, or enough random bits
Sortable by time Newer ids compare greater, so ORDER BY id ≈ ORDER BY created_at Time in the high bits, trusting clocks
Compact Fits a 64-bit integer column and index Few bits, so a tight budget
No per-id coordination Minting doesn’t wait on another machine Local generation
Not guessable (sometimes) Public ids don’t reveal volume or let you enumerate records Randomness, opaque encodings

You can’t have all five at once: 64 bits is too few for safe randomness, and time ordering leaks creation time.

In scope (typical) Ask about
Functional next_id() returns a unique 64-bit integer; ids are roughly ordered by creation time; ids can be decoded into (time, worker, sequence) for debugging. Must ids be strictly increasing, or is “roughly” (k-sorted) fine? 64 or 128 bits? Numeric or string?
Scale 200k ids/s fleet-wide at peak, a few thousand processes, bursty per process. Peak per process, not just total? How many processes, including during deploys?
Latency Microseconds; no network call per id. Is a batched network call acceptable?
Lifetime Ids stay valid and ordered for decades. Expected system lifetime?
Availability Minting continues if any shared component is down. Can a process start up while the coordinator is down?
Out of scope Human-friendly short codes (that’s the URL shortener), gap-free numbering (invoice numbers need a ledger, not this).

The bit budget is fixed at 63 usable bits. The question is whether your busiest process fits in the sequence field and your process count fits in the worker field, with room to spare during deploys.

Back of the envelope
drag the assumptions
assumptions
so you need
worker ids in use39.1%restarts and deploys need spare ids while leases expire
busiest process: ids per ms needed2.5
busiest process: share of sequence space0.06%above 100% it stalls until the next ms
lifespan of the time field (years)69.7
index space saved vs 128-bit UUID keys800 GBper index that contains the key; secondary indexes repeat it
200k ids/s over 400 processes is 500/s each, and with 5× skew the busiest needs about 2.5 ids per ms: well under 4,096. The worker field is the tighter one: 400 of 1,024 ids in use, and a rolling deploy can briefly double that. Drag processes past 1,000 and it fails.

What to say out loud: “Per-process throughput is nowhere near the 4,096-per-millisecond ceiling. The constraint is worker ids: about 400 processes now, double during deploys, against 1,024. I’d keep 10 worker bits but assign them with leases that expire, so dead pods give their ids back.”

pseudocode: the contract
next_id() -> int64 -- local call, no I/O
decode(id) -> (time_ms, worker, sequence) -- for logs and debugging only
64-bit layout (Twitter Snowflake):
bit 63 0 -- sign bit unused, ids stay positive
bits 62..22 milliseconds since epoch -- 41 bits ≈ 69.7 years
bits 21..12 worker id -- 10 bits = 1,024 workers
bits 11..0 sequence within the ms -- 12 bits = 4,096 ids/ms/worker
On the wire: a decimal string, e.g. "2006515713438650368"

Two decisions hide in that sketch:

  • Ordering is “k-sorted”, not strict. Two workers minting in the same millisecond are ordered by worker id, not by which came first, and clocks on different machines differ by a few ms. So ids are ordered across the fleet only to within clock skew. Within one worker they are strictly increasing. If a consumer needs a strict total order, it needs a single sequencer (one partition, one log), not a cleverer id.
  • Ids leak information. A time-ordered id reveals when a record was created, and a per-worker sequence reveals volume. For public-facing ids that must not be enumerable (invoice links, share tokens), use a separate random token, or encrypt the id with a block cipher before exposing it.

There is almost no “system” on the hot path: the generator is a library. The design is the startup handshake, the clock, and the fallbacks.

Unique ids: claim, mint, misbehaving clocks, ranges
step through
flow
The only coordination Snowflake needs happens once, at startup, off the request path.
lease worker idINSERT idnext rangeCaller (create order): clientCallercreate orderOrder service (id library · ×N): workerOrder serviceid library · ×NTime source (NTP / PTP): externalTime sourceNTP / PTPCoordinator (etcd / ZooKeeper): storeCoordinatoretcd / ZooKeeperOrders DB (B-tree on id): storeOrders DBB-tree on idTicket DB (hands out ranges): storeTicket DBhands out ranges
instancepod order-7f9c
worker bits10 → 1,024 ids
A new instance starts. Before it can mint ids it needs a worker id that no other live process holds; two processes with the same worker id would generate identical ids in the same millisecond.
1 / 3
Component Role Why it’s there
Id library (in every service) Reads the clock, keeps the last timestamp and sequence, packs the bits. Minting is a local operation: no network, no single point of failure.
Coordinator (etcd / ZooKeeper) Hands out worker ids under leases at startup. Uniqueness depends on no two live processes sharing a worker id.
Time source (NTP / PTP) Keeps clocks close together and moving forward. The time field is only as good as the clock.
Ticket DB (alternative) Hands out id ranges for schemes without time in the id. Small, dense, ordered ids when you can afford a round trip per range.
Scheme Size Sortable Coordination per id Main risk
DB AUTO_INCREMENT 64-bit Strictly Every insert, one DB Single writer; can’t shard
Ticket server (Flickr, 2010) 64-bit Per server One round trip per id (or per range) The ticket DB; Flickr ran two, one odd and one even
Range / segment allocation 64-bit Roughly One round trip per block Gaps after crashes; ordering only across blocks
UUIDv4 128-bit No None Index locality; size
UUIDv7 / ULID 128-bit By ms None Size; same-ms ids are random-ordered unless a counter is used
MongoDB ObjectId 96-bit By second None Seconds, not ms; per-process random + counter
Snowflake 64-bit By ms (k-sorted) None (worker id at startup) Clock going backwards; worker id reuse

Random ids need no coordination, but the birthday bound decides how many random bits are enough: collisions become likely around 2^(b/2) ids, not 2^b. Drag the rate:

Random ids: chance of any collision over time
interactive
11e-61e-121e-181e-241e-301 s1 min1 h1 day1 mo1 yr10 yr100 yr1 in a billiontime generating ids →
after 5 years (1.6e13 ids)
random 32-bit100%
random 64-bit100%
UUIDv7 (48-bit ms + 74 random)1 in 2.4e7
UUIDv4 (122 random)1 in 4.3e10
Snowflake, unique worker ids0 by construction
random 64-bit: 50% after ≈ 0.59 daysUUIDv4: 50% after ≈ 2.7e18 ids
The birthday bound: with n random ids drawn from 2^b values, collisions become likely around n ≈ 2^(b/2), not 2^b. 64 random bits sound huge but give out at about 5 billion ids. UUIDv7 spends 48 bits on time, so collisions can only happen between ids from the same millisecond; its risk grows with the rate (squared, per ms) more than with the total. Snowflake trades all randomness for a coordination step: a unique worker id per process.

Takeaways: 64 random bits are not enough for a busy system (a coin flip after about 5 billion ids); 122 bits (UUIDv4) are enough for anything. UUIDv7 can only collide within one millisecond, so its risk depends on the per-ms rate; RFC 9562 lets implementations use some of the random bits as a counter, which removes same-process collisions and keeps same-ms ids ordered.

The 63 bits are a budget, and every field trades against the others. Start from a real system’s layout, then fit it to requirements:

Plan a 64-bit id layout against your requirements
interactive
The original: 41 bits of milliseconds, 10 bits of machine, 12 bits of sequence.
1
time (1 ms ticks) · 41
datacenter 5 + worker 5 · 10
sequence · 12
bit 63bit 0
runs out in
2080
69.7 years from the epoch
workers
1,024
need 1,000
ids/s per worker
4.1M
need 10K at peak
ids/s whole fleet
4.2B
if every worker id is in use
An id minted on 1 Jan 2026 by worker 1: 2006515713438650368 (19 digits).
lifespan okenough worker idssequence covers peakabove 2^53: send as a string in JSON
timeworker / shardsequence
The 63 usable bits are a fixed budget. Every bit moved to the worker field doubles the fleet and halves the lifespan; every bit moved to the sequence doubles per-worker throughput and halves it too. A coarser tick stretches the lifespan by the same factor and divides throughput by it. Note the last chip: JavaScript numbers are exact only up to 2^53, so Twitter and Discord send these ids as strings.
System Time Worker / shard Sequence Notes
Twitter Snowflake 41 bits, ms, epoch Nov 2010 10 (5 datacenter + 5 worker) 12 4,096 ids/ms/worker; ~69 years
Discord 41 bits, ms, epoch 2015 10 (5 worker + 5 process) 12 Sent as strings in JSON
Instagram 41 bits, ms, epoch 2011 13 (logical shard) 10 Generated in PL/pgSQL inside each Postgres shard; the id says which shard holds the row
Sonyflake 39 bits, 10 ms units 16 (machine) 8 174 years, but only 25.6k ids/s per machine

Why the order matters. Time in the high bits means numeric order is time order. Putting the shard in the id (Instagram) means you can route a lookup by id with no directory. The distributed thinking page has a bit-level view of the packing with a clock-skew ordering demo.

Running out of sequence. If a worker uses all 4,096 sequence numbers in one millisecond, it must not wrap to 0 (that would repeat an id). The options: wait for the next millisecond (Twitter’s original), or treat the time field as a logical clock that may run slightly ahead of real time and catch up later. Either way, it’s rare if the sequence field fits your peak; the estimate above checks that.

Uniqueness rests on two assumptions: no two live processes share a worker id, and a worker’s clock never repeats a millisecond it already used. Both fail in practice.

Clocks that go backwards. NTP can step a clock back, a VM can resume from a pause, a leap second can repeat. Options, from strict to lenient:

Policy Behaviour Cost
Refuse Error until the clock passes the last timestamp used (Twitter’s original Snowflake threw “Clock moved backwards”) Requests fail for the gap; fine for ms, bad for minutes
Wait Sleep until the clock catches up Latency spike equal to the gap
Logical time Keep using the last timestamp (and its sequence) until real time catches up Ids drift ahead of wall time; must bound the drift
Change worker id Re-register under a new worker id, whose past ids can’t collide Uses up worker ids

Production systems combine them: tolerate a small step without failing, refuse and alert on a large one, persist the last timestamp so a restart doesn’t go back in time, and configure NTP to slew rather than step.

Assigning worker ids. Static config breaks with autoscaling. Deriving it from the IP address (Sonyflake uses the low 16 bits of a private IP) works until two hosts share those bits across networks. The robust option is a lease from a coordinator:

pseudocode: claim and keep a worker id
on startup:
for w in 0 .. 2^worker_bits - 1:
if coordinator.create_if_absent("/idgen/workers/" + w, owner = me, lease_ttl = 30 s):
worker_id <- w; break
if none free: fail startup -- out of worker ids: alert, don't guess
every 10 s:
if not coordinator.renew(lease): stop minting ids immediately
-- after the TTL another process may hold this worker id, so a process that
-- cannot renew must stop before its lease could have expired

Ranges instead of time. If ids need to be small and dense rather than time-ordered, hand out blocks from a counter:

pseudocode: range allocation with prefetch
take_block(size):
-- one atomic statement on the ticket DB; returns the new upper bound
UPDATE ranges SET next = next + size WHERE name = 'orders' RETURNING next
return [next - size, next)
next_id():
if current block is used up: switch to the prefetched block (or take_block now)
if less than 20% of current block left and no prefetch in flight: prefetch in background
return current.next++

The id is usually a primary key, so its shape affects the database more than the generator does.

  • B-tree locality. InnoDB clusters rows by primary key; Postgres keeps a B-tree index on it. Time-ordered keys append at the right edge: the hot leaf pages stay in cache and fill completely. Random UUIDv4 keys insert all over the tree: pages split and are left half full, and the working set becomes the whole index. This is the main practical reason to prefer UUIDv7 over v4.
  • Size. 8 bytes vs 16 per key, and every secondary index stores the primary key again. At 100 billion rows that’s hundreds of GB.
  • Sharding by id. Range-partitioning on a time-ordered id sends all new writes to the last shard. Hash-partition the id, or partition by another key and embed the shard id in the id itself (Instagram).
  • Exposing ids. Time-ordered ids are fine as internal keys; for public URLs consider a random token or an encrypted id so outsiders can’t infer volume (the “German tank problem”).
What breaks What you see Design response
Clock steps backwards Possible duplicate ids, or refusals Never mint below the last timestamp used; small gap: hold; large gap: refuse, alert, fail health checks; NTP slewing
Two processes share a worker id Duplicates in the same ms; unique-key violations Leased worker ids; stop minting when the lease can’t be renewed
Worker ids exhausted New instances fail to start Enough worker bits for 2× peak processes; short lease TTLs so dead ones free up
Sequence exhausted in a ms Calls stall for up to 1 ms Size the sequence for the busiest process; spread hot traffic over more processes
Coordinator down Running processes fine; new ones can’t start Keep the coordinator off the per-id path; cache the last lease for restarts within its TTL
Ticket DB down Minting stops once ranges run out Large ranges plus prefetch; two ticket servers (odd/even)
JS clients round ids Ids that don’t match; “not found” Serialize as strings
Epoch runs out Ids wrap or overflow Choose the epoch and tick for the system’s lifetime, then add margin
Operation Time Space
next_id (Snowflake) O(1): a clock read, compare, increment, shifts; ≈ tens of ns O(1): last timestamp + sequence per process
next_id (UUIDv4 / v7) O(1): random bytes (+ clock) none shared
next_id (ranges) O(1) amortised; one DB round trip per block one counter row per sequence name
Worker id claim O(W) tries worst case at startup; renew every few seconds one lease per process
decode(id) O(1) shifts and masks —
Key storage — 8 B (Snowflake) vs 16 B (UUID) per key, per index
Throughput ceiling 2^sequence_bits per tick per worker; 4.1M/s per worker for 12 bits at 1 ms —

A good 45-minute arc:

  1. Clarify (5 min). Sortable? 64 or 128 bits? Peak per process? How many processes? Public or internal?
  2. Options (10 min). Auto-increment, UUIDv4, ticket server, UUIDv7, Snowflake. One line each on why it does or doesn’t fit.
  3. Design Snowflake (10 min). The bit layout and what each field buys; capacity from the estimate; the epoch; strings on the wire.
  4. Hard parts (15 min). Clock going backwards, sequence exhaustion, worker id assignment with leases, what happens during a partition.
  5. Storage and stretch (5 min). B-tree locality, sharding by id, k-ordering limits.

Follow-ups to expect: “Can ids be strictly increasing across the fleet?” Only with a single sequencer; Snowflake is k-sorted within clock skew. “Multi-region?” Give each region a slice of the worker id space (that’s what Twitter’s 5 datacenter bits are). “How do you test it?” Inject the clock: step it back, freeze it, and drive 5,000 calls into one ms; property-test that ids are unique and increasing per worker; run two generators with the same worker id in a test and see the duplicate.

Check yourself
pick one
A service generates random 64-bit ids at 10,000 per second. About when does the chance of at least one collision reach 50%?
Check yourself
pick one
Two processes in a Snowflake fleet were accidentally given the same worker id. When can they produce a duplicate id?
Check yourself
pick one
Ids from workers A and B are sorted by value. A's clock runs 3 ms ahead of B's. What can you say about the order?

Concept pages to drill: Bit manipulation · Time handling · Distributed thinking · Randomization · Indexing · Edge cases