Architecture

App service In-process UUIDv4 / v7, ULID, KSUID Snowflake worker timestamp + machine_id + per-ms sequence Allocator (batch) Postgres SEQUENCE, range pre-allocation Coordinator ZooKeeper / etcd machine-id assign NTP / chronyd clock sync, drift monitoring

Capacity Estimation

MetricValueNotes
IDs/s peak~10 Macross fleet
Per-process throughput~1 M/sUUIDv4 in-mem
Snowflake per-machine throughput~4 M/s12-bit sequence per ms
ID size64 b (Snowflake), 128 b (UUID)2× storage difference
Time-ordered prefix~ms resolutionSnowflake, UUIDv7, KSUID, ULID
Allowable clock skew± 1 sSnowflake assumption

Snowflake

Twitter Snowflake: a 64-bit ID composed as (timestamp_ms || machine_id || sequence) with bit allocations like 41 + 10 + 12. Properties:

  • Time-ordered — sortable by creation time. Indexes append at the tail (no random insert on B-tree).
  • Coordination only at startup — each machine reserves a unique machine_id from ZooKeeper / etcd; runtime allocation is in-process.
  • 4096 IDs/ms/machine — sequence overflow blocks until the next millisecond.
  • 69 years of timestamp — with a custom epoch, you push the rollover problem far enough out.

Failure mode: clock goes backward (NTP step, leap second). Snowflake's standard handling: refuse to generate IDs until the clock catches up (block briefly) or alarm and shut down. Without this guard, two IDs can collide at the same (timestamp, machine_id, sequence).

UUIDv4 vs v7

  • UUIDv4 — 122 random bits. Extremely unlikely collision (261 generations before 1% probability). No coordination, no time component.
  • UUIDv7 — 48-bit Unix millisecond timestamp + 74 random bits. Time-ordered like Snowflake but no machine_id. RFC 9562 (2024).

The case for v7 over v4: B-tree indexes on UUIDv4 keys fragment because new IDs land in random pages. UUIDv7's leading timestamp gives append-mostly inserts, dramatically reducing index bloat in Postgres and MySQL. Use v7 by default in any new system.

KSUID

Segment's K-Sortable Unique ID: 32-bit timestamp (seconds, custom epoch 2014) + 128 random bits, encoded as 27-character base62. Pros:

  • Sortable lexicographically — works as a sortable text column without binary-aware tooling.
  • Compact text representation — 27 chars vs UUID's 36.
  • No coordination — pure-process generation.

Best when IDs flow as text (REST APIs, URLs) and you want time-ordering. Less efficient than 64-bit Snowflake when you index on it as binary.

ULID

Universally Unique Lexicographically Sortable Identifier: 48-bit Unix millisecond + 80 random bits, encoded base32 (Crockford's). 26 characters. Properties:

  • Time-ordered with millisecond precision.
  • Monotonic within the same millisecond — if two IDs generate in the same ms, the second increments the random portion. Strict same-process monotonicity, eventual cross-process.
  • Crockford base32 avoids ambiguous characters (no 0/O, 1/I/L).

ULID and UUIDv7 occupy similar territory; UUIDv7 is the standard answer (it has an RFC). ULID is fine and existed first.

The Clock Skew Problem

Any time-prefix scheme depends on monotonic time. Failure modes:

  • NTP step backward — clock jumps to past time; new IDs collide with already-issued ones. Defense: detect step (compare wall vs monotonic clock); refuse to issue or shift to a sequence-only mode until reconciled.
  • VM live migration pause — clock jumps forward by seconds. Mostly harmless (gaps in timestamps); becomes a problem only if you exceed the timestamp encoding width unexpectedly.
  • Leap second — depending on OS, clock either stalls or steps. Stall is fine; step is not. Modern Linux smear-leap-second avoids the worst.

Operationally: monitor NTP drift, alert at 100 ms divergence; ensure all hosts use the same sync source; consider PTP for sub-millisecond consistency in tight clusters.

Distributed Coordination Tradeoffs

  • None (UUIDv4) — trivial; no time-order; large.
  • Startup-only (Snowflake, machine_id assignment) — ZooKeeper/etcd at boot; runtime is in-process.
  • Per-batch allocator — service grabs a range of IDs (1000) from a Postgres SEQUENCE; replenishes when low. Coordination amortized.
  • Per-ID allocator — every generation is a network call to a central counter. Highest accuracy of monotonicity, lowest throughput.

Most production systems pick "startup-only" or "per-batch": 100× the throughput of per-ID with negligible monotonicity loss.

Failure Modes

  • Two machines with the same machine_id — ZK lease race during deploy. Issue lease via etcd CAS; verify on each ID generation; refuse if invalidated.
  • Sequence overflow — high-burst service exceeds 4096/ms budget. Snowflake stalls until next ms; fix by widening sequence bits or sharding the workload.
  • Database key fragmentation from UUIDv4 — switch to v7 / Snowflake.
  • Replicated DB with conflicting IDs — two regions both generate ID X. Use machine_id partitioning to ensure region-disjoint ID spaces.

FAQ

Snowflake or UUIDv7?

UUIDv7 if you want a standard format and don't need machine-id semantics. Snowflake if you need 64-bit IDs (cheaper indexes), per-machine sequence guarantees, and you have the operational maturity to manage machine-id allocation.

Why not just use auto-increment from the database?

Single-master scaling ceiling; cross-region active-active is a pain. Auto-increment is fine for single-region small to medium services; distributed schemes win past that.

How do you avoid leaking creation rate?

Sequential IDs let competitors estimate your daily order count. Defenses: random ID (UUIDv4), or hash the ID before exposing externally; keep the sequential one internal.

Can I use these as primary keys?

Yes. Snowflake / UUIDv7 are designed for it. UUIDv4 works but causes index bloat; if you must, Postgres's uuid_generate_v7() is a drop-in fix.