Designing a Distributed ID Generator
Design a unique ID generator producing time-sortable 64-bit IDs across data centers using Snowflake-style bit layout, clock handling and worker coordination.
Every database row, order and event needs a unique identifier. Auto-increment columns stop working once data is sharded across machines. This article designs a service that issues globally unique, roughly time-ordered IDs without central coordination on each request.
Requirements
Functional
- Generate unique IDs across many machines and data centers.
- IDs fit in 64 bits (for compact indexes and JavaScript-safe handling when represented as strings).
- IDs are roughly sortable by creation time.
- Numeric only, no embedded personal data.
Non-functional
- 100,000 IDs per second per machine, one million per second globally and growing.
- p99 generation latency under 1 ms.
- 99.999 percent availability: ID generation is on the critical path of every write.
- No duplicates, ever.
- Operates for at least 69 years.
Capacity estimation
One million IDs per second is 86 billion per day. 64 bits of address space is about 1.8 times 10 to the 19, enough for 200 million years at that rate, so space is not the constraint; bit allocation is. Network: if IDs are generated in-process by a library, there is no network cost. If a service issues them, one million requests per second at 100 bytes is about 100 MB/s, trivial to spread over 20 instances.
Approaches compared
- UUID v4: 128 bits, no coordination, but random, so B-tree inserts scatter and fragment indexes. UUID v7 adds a timestamp prefix and fixes locality but is still 128 bits.
- Database ticket server: auto-increment on a dedicated database (Flickr). Simple, ordered, but a bottleneck and single point of failure unless run as two servers with odd and even steps.
- Range allocation: a service hands each application server a block of 10,000 IDs; servers dispense locally. Fast, but IDs are not time-ordered across servers and unused ranges are lost on crash.
- Snowflake-style: compose timestamp, machine id and sequence into 64 bits. No per-request coordination. This is our choice.
High-level design
Bit layout (64 bits):
| 0 | 41 bits timestamp ms | 5 dc | 5 worker | 12 bit sequence |
sign since custom epoch 10 bits machine id per-ms counter
App servers --> ID library (embedded) or ID service cluster
|
Worker-id registry (ZooKeeper / etcd)
assigns unique machine ids with leases
NTP / time sync on every host
Data model
There is almost no stored data. The generator holds in memory: last timestamp, current sequence, its machine id and the custom epoch (for example 2026-01-01). The registry stores a mapping from machine id to host and lease expiry. Optionally, a durable high-water mark of the last issued timestamp is written periodically for crash safety.
Key deep-dives
Bit budget. 41 bits of milliseconds is 2.2 trillion ms, about 69.7 years from the custom epoch. 10 bits of machine id supports 1,024 generators. 12 bits of sequence gives 4,096 IDs per millisecond per machine, or about 4 million per second per machine, far above the 100K target. Rebalance the split if needs differ: more machines means fewer sequence bits.
Generation algorithm. Read the current millisecond. If equal to the last timestamp, increment the sequence; if it overflows, spin until the next millisecond. If greater, reset the sequence to zero. Compose with shifts and ORs. The operation is lock-free with an atomic or a single mutex, taking well under a microsecond.
Machine id assignment. Collisions in machine id are the main way to break uniqueness. Assign ids through a coordination service using ephemeral nodes or leases, so a restarted pod gets a unique id and a zombie cannot keep one. In Kubernetes, a StatefulSet ordinal is a simple stable source.
Clock handling. Wall clocks can step backward after NTP corrections. If the current time is less than the last timestamp, the generator must not issue IDs. Small regressions (a few milliseconds) can be waited out; larger ones should raise an error and take the node out of rotation. Persisting the last timestamp on shutdown and refusing to start earlier than it protects against restart regressions. Use slewing rather than stepping clocks, and monitor drift.
Ordering guarantees. IDs are time-sortable to roughly millisecond precision across machines, bounded by clock skew, but not strictly monotonic globally. If strict causal ordering is required, use a logical clock or a single-leader sequencer, which costs availability.
Variants. Twitter Snowflake, Sonyflake (10 ms units, longer lifetime) and ULID (128 bits, base32 text) all adapt this recipe. Hide IDs from enumeration attacks by never using them as secrets.
Failure modes and trade-offs
- Duplicate machine ids: causes duplicate IDs. Prevent with leases and a registry that is consulted at startup only, so runtime has no dependency.
- Registry outage: running generators continue; only new nodes cannot start.
- Clock jump forward: burns timestamp range and breaks nothing but ordering assumptions; cap acceptable jumps.
- Sequence exhaustion: brief spin-wait; add machines if sustained.
- Embedded library versus service: a library avoids network hops and a failure domain but spreads machine ids widely; a dedicated service centralizes control with one extra RPC.
- Predictability: time-ordered IDs leak creation time and volume to competitors, so expose opaque public identifiers if that matters.
Key takeaways
- Snowflake-style IDs trade a small coordination step at startup for zero coordination per request.
- Allocate bits deliberately: timestamp for lifetime, machine bits for fleet size, sequence for burst rate.
- Unique machine ids and monotonic clocks are the two invariants; enforce both defensively.
- Prefer time-ordered IDs for database locality over random UUIDs.
- Time-sortable means approximately ordered, not globally strictly ordered.