Generating unique IDs at scale across distributed servers — a practical guide to UUIDs, Snowflake, UUIDv7, and how to choose between them.
Twitter mints 6,000 IDs every second. Discord routes 4 billion messages a day — each with a unique ID. None of them ask a database for these IDs. Why? And what do they do instead?
The simplest way to give every row a unique number is a database AUTO_INCREMENT column. The database keeps a counter and hands out 1, 2, 3, … It works perfectly when there's one database.
Now imagine your app grows. You add a second server. A third. Maybe a second region. Two questions appear:
Every server pings the database for the next ID. Easy to understand, but the database becomes a bottleneck — every write waits its turn. At a few thousand writes per second, this falls apart. Database goes down → no one can write anything.
No coordination needed, but Server A and Server B will both eventually pick the number "1234". You silently corrupt your data and don't notice for months.
A UUID (Universally Unique Identifier) is a 128-bit number, written as 32 hex characters with hyphens. You've seen them everywhere:
550e8400-e29b-41d4-a716-446655440000 // 32 hex chars = 128 bits = a really, really big number
The trick: 128 bits is so large (about 5 followed by 36 zeros) that if you generate one at random, the odds of generating the same one twice — anywhere on Earth, by anyone — are essentially zero. No coordination needed. That's the whole pitch.
128 bits of randomness. The default everywhere. What crypto.randomUUID() in your browser gives you. Used for session tokens, idempotency keys, anything that just needs to be unique and unguessable.
Same size, but the first 48 bits are a timestamp. So when you sort UUIDv7s, they come out in the order they were created. Best of both worlds — unique and sortable.
UUIDs are great. But they're not always the right answer. Here's a clean breakdown.
In 2010, Twitter hit a wall: their database couldn't keep up with assigning tweet IDs. They invented Snowflake — a way for any server to mint a unique 64-bit ID locally, without asking anyone.
Milliseconds since some chosen start date. Two IDs from the same server can't collide unless they're in the same millisecond.
Each server gets a unique number (0–1023). Two different servers can't collide because their machine IDs differ.
If a server mints multiple IDs in the same millisecond, the counter goes 0, 1, 2, … up to 4096. Then it waits for the next ms.
Put it together: same server + same millisecond + same counter is impossible. By construction, two Snowflake IDs can never be equal. No coordination on the hot path — every server just stamps its own clock + machine + counter.
The whole Snowflake design rests on one assumption: every server has a different machine ID. So how do we hand out 1024 unique numbers across servers — including new ones spinning up, old ones dying — without collisions?
When a server starts up, it asks a small coordinator service (ZooKeeper, etcd, or Redis) for an unused machine number. The coordinator picks one, marks it taken, and returns it. The server caches that number and uses it for the rest of its life.
The key: the coordinator is only on the boot path. Once a server has its machine number, it never talks to the coordinator again. So if the coordinator goes down, existing servers keep minting IDs just fine — only new server startups are blocked.
Snowflake is brilliant but it has a cost: you need a coordinator. If you don't want to run one, UUIDv7 is the modern answer.
The shape: take a regular UUID, but force the first 48 bits to be a Unix-millisecond timestamp. The remaining bits are random. Because the timestamp leads, sorting UUIDv7s sorts them by creation time — fixing the #1 weakness of UUIDv4.
Many big companies have their own Snowflake-flavored ID scheme. Same idea, different bit splits, tuned for their constraints.
| Name | Size | Used by | Why |
|---|---|---|---|
| Twitter Snowflake | 64 bits | Twitter (original) | The reference design |
| Discord | 64 bits | Discord | Sortable IDs for fast debug |
| 64 bits | Embeds shard ID for sharded DB | ||
| MongoDB ObjectId | 96 bits | MongoDB | Built into Mongo, no setup |
| UUIDv7 | 128 bits | RFC 9562 (modern std) | No coordinator, sortable |
| ULID | 128 bits | Many startups | Like UUIDv7 but base32-encoded (26 char string) |
Each of these has shipped to production at a real company.
You set the machine ID via an env var, deploy two pods, both get "1". Now they collide. Always use a coordinator (ZooKeeper / Redis) to hand out machine IDs at boot.
NTP corrects the clock and time goes back by 1 ms. The server may mint an ID it already minted. Fix: track the last timestamp used; refuse to issue IDs until the clock catches up.
Math.random() for UUIDsIt's not cryptographically secure. Use crypto.randomUUID() in JS, UUID.randomUUID() in Java, uuid.uuid4() in Python.
Sequential IDs leak business volume — competitors can infer your QPS. Use a hashed external ID for public URLs, Snowflake internally.
Random inserts fragment the B-tree index. Inserts that took 5 ms start taking 50 ms. Switch to UUIDv7 or Snowflake.
VARCHAR(36)You're storing 36 bytes when 16 will do. Use BINARY(16) in MySQL or the native uuid type in Postgres.
If you remember nothing else, remember this table.
| Use case | Pick | Why |
|---|---|---|
| Session tokens, API keys | UUIDv4 | Unique + unguessable, sorting not needed |
| Idempotency keys | UUIDv4 | Client-generated, no coordinator |
| External / public IDs | UUIDv4 | Don't want to leak row counts |
| New project's primary keys | UUIDv7 | Sortable + no coordinator + drop-in for v4 |
| Existing system, billions of rows | Snowflake | Storage matters; you can run a coordinator |
| Mobile / offline-first writes | UUIDv7 | Phone has no coordinator; still want sortable |
| Just learning, simple app | DB AUTO_INCREMENT | Simplest thing that works at low scale |