Chapter 27: Distributed ID Generation
Why Distributed IDs
Auto-increment primary keys fail in distributed systems. When data is sharded across multiple databases, each shard generates its own sequence, causing ID collisions. Distributed ID generation solves this by creating globally unique identifiers without coordination.
Requirements
| Requirement | Why It Matters | Trade-offs |
|---|---|---|
| Uniqueness | No collisions across all nodes globally | Coordination adds latency |
| Ordered | Time-sortable for pagination, range queries, B-tree indexes | Random IDs fragment indexes |
| Compact | Small size (64-bit fits in a long, reduces storage/memory) | 128-bit UUIDs are larger |
| Fast | Generate millions per second without blocking | Network calls for coordination slow generation |
| Coordination-free | No central bottleneck or single point of failure | Pure coordination-free approaches risk collisions |
Why Auto-Increment Fails
ID: 1,2,3...] A --> C[DB Shard 2
ID: 1,2,3...] D[App Server 2] --> B D --> C style B fill:#ff6b6b,color:#1a1a2e style C fill:#ff6b6b,color:#1a1a2e
Both shards generate ID=1, causing collisions when aggregating data.
UUID (Universally Unique Identifier)
128-bit identifiers standardized in RFC 4122. Multiple versions with different generation strategies.
UUID Versions
| Version | Structure | Pros | Cons |
|---|---|---|---|
| v1 | Timestamp + MAC address + counter | Time-sortable, low collision risk | Exposes MAC address (privacy risk), non-monotonic across nodes |
| v4 | 122 bits random + 6 bits version/variant | No coordination, simple, privacy-safe | Not sortable, fragments B-tree indexes, slower inserts |
| v6 | Reordered v1 (time MSB-first) + MAC + counter | Time-sortable, maintains v1 uniqueness | Still exposes MAC, draft spec (not widely adopted) |
| v7 | Unix timestamp (48-bit ms) + random (74 bits) | Time-sortable, no MAC exposure, good for databases | Draft spec, less adoption than v4 |
Example UUIDs
v1: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
v4: 550e8400-e29b-41d4-a716-446655440000
v7: 017f22e2-79b0-7cc3-98c4-dc0c0c07398f
When to Use
- v4: Client-side ID generation (offline-first apps), no ordering needed
- v7: Database primary keys where time-ordering improves insert performance
Twitter Snowflake
64-bit ID scheme designed by Twitter for high-volume distributed ID generation. Generates roughly time-sorted IDs without coordination.
Structure
1 bit
always 0] --> B[Timestamp
41 bits
ms since epoch] B --> C[Datacenter ID
5 bits
0-31] C --> D[Machine ID
5 bits
0-31] D --> E[Sequence
12 bits
0-4095]
| Component | Bits | Purpose | Range |
|---|---|---|---|
| Sign | 1 | Reserved (always 0 for positive long) | 0 |
| Timestamp | 41 | Milliseconds since custom epoch (e.g., 2010-11-04) | ~69 years |
| Datacenter | 5 | Datacenter identifier | 0-31 (32 datacenters) |
| Machine | 5 | Machine/server identifier within datacenter | 0-31 (32 machines per DC) |
| Sequence | 12 | Per-machine counter (resets each millisecond) | 0-4095 (4096 IDs/ms/machine) |
Properties
- Capacity: 4096 IDs per millisecond per machine = 4.096M IDs/sec/machine
- Scalability: 1024 machines (32 DC × 32 machines) = 4.2B IDs/sec globally
- Ordering: Roughly time-sorted (same timestamp = same prefix)
- Lifetime: 41-bit timestamp lasts 69 years from custom epoch
Generation Algorithm
class SnowflakeIDGenerator {
private long epoch = 1288834974657L; // Custom epoch
private long datacenterIdBits = 5L;
private long machineIdBits = 5L;
private long sequenceBits = 12L;
private long datacenterId;
private long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = currentTimeMillis();
// Clock moved backward - reject
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards");
}
// Same millisecond - increment sequence
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 4095; // Mask to 12 bits
if (sequence == 0) {
// Sequence overflow - wait for next ms
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L; // New millisecond - reset
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << 22)
| (datacenterId << 17)
| (machineId << 12)
| sequence;
}
}
Clock Skew Handling
If system clock moves backward (NTP correction), Snowflake rejects ID generation to prevent duplicate IDs. Alternative: wait until clock catches up, but adds latency.
ULID (Universally Unique Lexicographically Sortable Identifier)
128-bit identifier designed for time-sorting and readability. Encoded in Crockford Base32 (no ambiguous characters like 0/O, 1/I/l).
Structure
| Component | Bits | Purpose | Details |
|---|---|---|---|
| Timestamp | 48 | Unix timestamp in milliseconds | ~8900 years (until year 10889) |
| Randomness | 80 | Cryptographically random bits | 2^80 combinations per millisecond |
Example
01ARZ3NDEKTSV4RRFFQ69G5FAV
|----------|-----------|
Timestamp Randomness
(10 chars) (16 chars)
ULID vs UUID v7
| Feature | ULID | UUID v7 |
|---|---|---|
| Size | 128 bits | 128 bits |
| Encoding | Base32 (26 chars) | Hex with dashes (36 chars) |
| Timestamp | 48 bits (ms) | 48 bits (ms) |
| Randomness | 80 bits | 74 bits (6 bits for version/variant) |
| Sortability | Lexicographically sortable as string | Binary sortable, string-sortable if unformatted |
| Standardization | Community spec | RFC draft (IETF) |
When to Use
- APIs where IDs are exposed as strings (URLs, JSON)
- Need case-insensitive, unambiguous encoding
- Time-sortable without binary parsing
Database Sequences
Traditional approach: database auto-increment. Works for single databases, fails in distributed systems. Solutions add complexity.
Single Database Auto-Increment
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255)
);
Problem: Single point of failure, coordination bottleneck, doesn't scale horizontally.
Flickr's Ticket Server
Two MySQL databases configured with different auto-increment offsets generate non-overlapping IDs.
auto_increment_offset=1
auto_increment_increment=2
1,3,5,7...] A --> C[Ticket DB 2
auto_increment_offset=2
auto_increment_increment=2
2,4,6,8...]
Configuration
-- DB 1
SET @@auto_increment_offset = 1;
SET @@auto_increment_increment = 2;
-- DB 2
SET @@auto_increment_offset = 2;
SET @@auto_increment_increment = 2;
Pros and Cons
| Pros | Cons |
|---|---|
| Simple, uses existing DB features | Doesn't scale beyond a few servers |
| Sequential IDs (good for ordering) | Network round-trip for each ID (latency) |
| No custom code | Single point of contention (each DB) |
| High availability (two DBs) | Adding more servers requires reconfiguration |
Pre-allocating ID Ranges
Optimization: allocate ranges (e.g., 1000 IDs at a time) to each app server, generate locally.
-- Server 1 claims 1-1000
-- Server 2 claims 1001-2000
-- Server 3 claims 2001-3000
Trade-off: Gaps in sequences if server crashes before using all IDs.
MongoDB ObjectID
12-byte (96-bit) identifier used as MongoDB's default _id. Combines timestamp, machine identifier, process ID, and counter.
Structure
4 bytes
Unix seconds] --> B[Machine ID
3 bytes
hostname hash] B --> C[Process ID
2 bytes] C --> D[Counter
3 bytes
random start]
| Component | Bytes | Purpose | Details |
|---|---|---|---|
| Timestamp | 4 | Unix timestamp in seconds | Good until 2106 |
| Machine ID | 3 | Hash of machine hostname | Unique per machine |
| Process ID | 2 | Process ID of MongoDB instance | Unique per process on machine |
| Counter | 3 | Incrementing counter | Random initial value, 16M IDs/sec |
Example
507f1f77bcf86cd799439011
|-------|-----|---|------|
Timestamp Machine PID Counter
(8 hex) (6 hex) (4) (6 hex)
Properties
- Time-sorted: Leading timestamp ensures rough ordering
- Compact: 12 bytes vs 16 for UUID
- Coordination-free: Machine+Process+Counter ensures uniqueness
- Limitation: 1-second granularity (vs millisecond for Snowflake/ULID)
Instagram's Approach
Instagram generates 64-bit IDs using PostgreSQL schemas per shard. Embeds shard ID in the identifier for routing.
Structure
| Component | Bits | Purpose |
|---|---|---|
| Timestamp | 41 | Milliseconds since custom epoch |
| Shard ID | 13 | Identifies which database shard (0-8191) |
| Sequence | 10 | Auto-increment per shard (0-1023) |
PostgreSQL Schema
CREATE SCHEMA insta5; -- Shard 5
CREATE SEQUENCE insta5.table_seq;
CREATE OR REPLACE FUNCTION insta5.next_id(OUT result bigint) AS $$
DECLARE
our_epoch bigint := 1314220021721;
seq_id bigint;
shard_id int := 5;
BEGIN
SELECT nextval('insta5.table_seq') % 1024 INTO seq_id;
result := (EXTRACT(EPOCH FROM clock_timestamp()) * 1000 - our_epoch) << 23;
result := result | (shard_id << 10);
result := result | (seq_id);
END;
$$ LANGUAGE PLPGSQL;
Advantages
- Shard-embedded: Extract shard ID from ID itself for routing
- PostgreSQL-native: Leverages existing infrastructure
- Time-sorted: Leading timestamp for chronological ordering
Routing Example
def get_shard_id(id):
return (id >> 10) & 8191 # Extract 13-bit shard ID
photo_id = 1234567890123456
shard = get_shard_id(photo_id) # 5
# Query shard 5 directly
Clock Skew
Time-based ID generation relies on synchronized clocks. Clock drift or NTP corrections cause issues.
Problems
| Issue | Impact | Example |
|---|---|---|
| Clock moves backward | Duplicate IDs if timestamp repeats | NTP correction subtracts 500ms |
| Clock drift | IDs out of order across nodes | Node A is 2 seconds ahead of Node B |
| Leap seconds | Time jumps forward or repeats | UTC leap second adjustment |
Snowflake's Approach
if (timestamp < lastTimestamp) {
// Clock moved backward - reject
throw new RuntimeException(
"Clock moved backwards. Refusing to generate ID"
);
}
Trade-off: Availability hit (service errors until clock catches up) to prevent duplicate IDs.
Alternative: Wait for Clock
if (timestamp < lastTimestamp) {
// Spin until clock catches up
while (currentTimeMillis() <= lastTimestamp) {
Thread.sleep(1);
}
timestamp = currentTimeMillis();
}
Trade-off: Adds latency (blocks ID generation).
Google Spanner's TrueTime
Uses GPS and atomic clocks to bound clock uncertainty. API returns time interval [earliest, latest].
TT.now() = [t_earliest, t_latest]
uncertainty = t_latest - t_earliest // Typically < 10ms
Spanner waits out uncertainty before committing transactions to ensure external consistency.
Practical Mitigations
- NTP: Keep clocks synchronized (ntpd, chrony). Monitor drift.
- Monotonic clocks: Use system monotonic clock (doesn't go backward) for intervals, wall clock for timestamps
- Hybrid clocks: Combine physical and logical clocks (Hybrid Logical Clocks in CockroachDB)
Comparison Matrix
| Strategy | Size | Sortable | Collision Risk | Generation Speed | Coordination | Use Case |
|---|---|---|---|---|---|---|
| UUID v4 | 128-bit | No | Extremely low (2^122) | Very fast (local) | None | Client-side, offline apps, no ordering needed |
| UUID v7 | 128-bit | Yes (time) | Extremely low | Very fast (local) | None | Database PKs, time-ordered UUIDs |
| Snowflake | 64-bit | Yes (rough time) | None (if config correct) | Very fast (4M/sec/node) | Machine ID config | High-volume event IDs (Twitter, logs) |
| ULID | 128-bit | Yes (time) | Extremely low (2^80) | Very fast (local) | None | APIs, user-facing IDs, string sorting |
| DB Sequence | 64-bit | Yes (strictly) | None (DB guarantees) | Moderate (network) | Database | Small-medium scale, existing DB infra |
| MongoDB ObjectID | 96-bit | Yes (rough time, 1s) | None (if config correct) | Very fast (local) | Machine config | MongoDB documents |
| 64-bit | Yes (time) | None (DB sequence) | Fast (DB call) | PostgreSQL per shard | Sharded PostgreSQL, routing in ID |
Performance Comparison
| Strategy | IDs/sec (single node) | Latency | Bottleneck |
|---|---|---|---|
| UUID v4/v7 | 10M+ | < 1μs | CPU/RNG |
| Snowflake | 4M | < 1μs | Sequence overflow (4096/ms) |
| ULID | 10M+ | < 1μs | CPU/RNG |
| DB Sequence | 10K-100K | 1-10ms | Network + DB lock |
| MongoDB ObjectID | 10M+ | < 1μs | Counter overflow |
Choosing the Right Strategy
Simple, fast, no coordination] B -->|Yes| D{What size?} D -->|128-bit OK| E{String or binary?} E -->|String in APIs| F[ULID
Base32, readable] E -->|Binary in DB| G[UUID v7
Standard, time-sorted] D -->|Need 64-bit| H{Have coordination service?} H -->|No| I[Snowflake
Requires machine ID config] H -->|Yes, using PostgreSQL| J[Instagram approach
Shard ID in ID] H -->|Yes, using MongoDB| K[ObjectID
Built-in] H -->|Legacy DB| L{Can you scale it?} L -->|Yes| M[DB Sequence
Flickr ticket server] L -->|No| N[Migrate to Snowflake/ULID]
Decision Framework
| Requirement | Recommendation | Why |
|---|---|---|
| Client-side generation (offline apps) | UUID v4 | No server dependency |
| Database primary keys (high write volume) | UUID v7 or ULID | Time-ordered reduces index fragmentation |
| Event logging (millions/sec) | Snowflake | Compact 64-bit, time-sorted, proven at scale |
| User-facing IDs in URLs/APIs | ULID | Readable, unambiguous, sortable as string |
| Sharded DB with routing in ID | Instagram approach | Shard ID embedded for direct routing |
| Small-medium scale (< 10K writes/sec) | DB Sequence | Simple, uses existing infrastructure |
| MongoDB | ObjectID | Built-in, optimized for Mongo |
Anti-Patterns
- UUID v4 for high-write DB PKs: Random IDs fragment B-tree indexes, killing insert performance
- Single DB sequence for distributed system: Coordination bottleneck, single point of failure
- Snowflake without clock monitoring: Clock skew causes ID generation failures or duplicates
- Exposing internal auto-increment IDs: Leaks info (creation order, total count), enumeration attacks
Migration Strategy
Migrating from auto-increment to distributed IDs:
- Change ID column type (INT → BIGINT for Snowflake, CHAR(26) for ULID)
- Dual-write: generate both old and new IDs during transition
- Update foreign keys to reference new IDs
- Backfill old records with new IDs (or keep old IDs as legacy)
- Switch reads to new IDs
- Drop old ID column
- Clarify requirements: ordering? size? scale? coordination available?
- Propose 2-3 options with trade-offs
- Recommend one based on requirements
- Discuss clock skew, collision risk, and failure modes
Summary
- Auto-increment fails in distributed systems due to collisions across shards
- UUID v4: 128-bit random, no coordination, not sortable, fragments indexes
- UUID v7 / ULID: 128-bit time-ordered, no coordination, good for DB primary keys
- Snowflake: 64-bit (timestamp|datacenter|machine|sequence), 4M IDs/sec/node, rough time-sorting
- Database sequences: Flickr ticket server scales to moderate load, but coordination limits throughput
- MongoDB ObjectID: 96-bit (timestamp|machine|PID|counter), 1-second granularity
- Instagram: 64-bit with shard ID embedded for routing
- Clock skew: Backward clocks cause duplicates; Snowflake rejects, Spanner uses atomic clocks
- Choose based on: ordering needs, size constraints, coordination availability, and scale