System Design Fundamentals
Networking & APIs
Storage & Data Modeling
Partitioning, Replication & Consistency
Caching & Edge
Messaging & Streaming
Reliability & Operability
Security & Privacy
Unique ID Generation Mechanisms: Snowflake, ULID, and KSUID
Every distributed system needs a way to assign unique identifiers to records, events, and messages. The two obvious choices each have a serious flaw.
Auto-increment IDs require a single counter that every node must talk to before writing. That counter becomes a throughput bottleneck and a single point of failure. If you run five microservices writing to different databases, coordinating a global sequence across all of them adds latency and coupling.
Random UUID v4 solves coordination by generating 122 bits of randomness locally. No central service needed. But randomness destroys index locality. A B-tree index on a UUID v4 primary key scatters inserts across the entire tree because each new ID lands at a random leaf page. The result is constant page splits, poor cache hit rates, and write amplification that grows with table size. A table with 100 million rows inserting UUID v4 keys can see 3-5x more disk I/O than one using sequential keys.
The fix is to embed a timestamp as the most significant bits of the identifier. This gives you three properties at once:
- Append-friendly writes — new IDs always land near the rightmost leaf of the B-tree, so inserts are nearly sequential.
- Chronological ordering — sorting by ID approximates sorting by creation time, which simplifies event replay, log merging, and debugging.
- Decentralized generation — each node can mint IDs locally using its own clock, with no coordination per ID.

The diagram above shows the contrast: random UUIDs scatter inserts across the entire B-tree, causing page splits at every level, while time-sorted IDs append to the right edge, keeping the hot working set small and cache-friendly.
The performance gap between random and time-sorted IDs grows with table size. At 10 million rows the difference is noticeable. At 1 billion rows, random UUID inserts can be 10x slower because the B-tree index no longer fits in memory and every insert triggers a random disk read to find the target leaf page.
This lesson covers three production-proven time-sorted ID formats:
- Snowflake (64-bit) — timestamp + machine ID + sequence counter. Compact numeric integer, ideal for SQL primary keys.
- ULID (128-bit) — timestamp + randomness. Crockford Base32 string, lexicographically sortable with millisecond precision.
- KSUID (160-bit) — timestamp + randomness. Base62 string, largest random space, second-level precision.
Each makes different trade-offs between size, precision, collision resistance, and operational complexity. By the end, you will know exactly which one to pick for a given design.
Snowflake was created at Twitter in 2010 to solve a specific problem: they needed to generate thousands of unique tweet IDs per second across hundreds of servers, and those IDs had to fit in a 64-bit integer so they could serve as efficient database primary keys.
The core idea is to split 64 bits into three segments that together guarantee uniqueness without any per-ID coordination.
Bit layout

- Sign bit (1 bit) — always 0, keeps the ID positive in signed 64-bit integer types.
- Timestamp (41 bits) — milliseconds since a custom epoch. Twitter uses November 4, 2010. With 41 bits you get 2^41 milliseconds, which is roughly 69.7 years from the epoch.
- Machine ID (10 bits) — identifies the generator node. Often split as 5 bits for datacenter + 5 bits for worker within that datacenter. This supports up to 1,024 distinct generators.
- Sequence (12 bits) — a counter that increments for each ID generated within the same millisecond on the same machine. 12 bits gives 4,096 values, so a single node can produce up to 4,096 IDs per millisecond (about 4 million per second).
The timestamp occupies the most significant bits, so numeric comparison of Snowflake IDs produces chronological order. Two IDs generated a millisecond apart will always sort correctly regardless of which machine produced them.
How generation works
Each generator node runs the following logic:
- Read the current time in milliseconds.
- If the current time equals the last recorded time, increment the sequence counter.
- If the sequence counter overflows (exceeds 4,095), busy-wait until the next millisecond and reset the sequence to 0.
- If the current time is greater than the last recorded time, reset the sequence to 0.
- If the current time is less than the last recorded time (clock went backwards), either wait for the clock to catch up or throw an error.
- Assemble the ID:
((timestamp - epoch) << 22) | (machine_id << 12) | sequence.
Clock skew is the biggest operational risk with Snowflake IDs. If NTP adjusts a server clock backwards by even 1ms, the generator must halt until time catches up or it will produce IDs with timestamps in the past, breaking monotonicity. Production deployments use monotonic clocks or track the maximum seen timestamp to avoid this. Sequence exhaustion (more than 4,096 IDs in 1ms on one node) is rare but causes the generator to block until the next millisecond.
Python implementation
The generated ID is a plain 64-bit integer. Store it as BIGINT in SQL databases. Numeric sorting on this column gives you chronological order, and range queries like WHERE id > X efficiently retrieve all records created after a specific time.
Variations in production
Twitter's original layout uses 5 datacenter + 5 worker bits. Discord adjusts the epoch and bit allocation to fit their growth projections. Instagram uses a PostgreSQL function with a different shard-bit strategy. The core pattern is always the same: time-major bits for sorting, node bits for uniqueness, sequence bits for throughput within a single millisecond.
ULID was designed as a drop-in replacement for UUID v4 that adds one critical property: lexicographic sorting equals chronological sorting. Like UUID v4, a ULID is 128 bits and needs no coordination. Unlike UUID v4, you can sort a column of ULIDs as plain strings and get creation-time order.
Structure
A ULID is 128 bits divided into two segments:
- Timestamp (48 bits) — milliseconds since Unix epoch (January 1, 1970). 48 bits of milliseconds span roughly 8,919 years, so this format will not overflow until the year 10,889.
- Randomness (80 bits) — cryptographically random bytes generated fresh for each ID (unless using monotonic mode, described below).

The 128 bits are encoded as a 26-character string using Crockford's Base32 alphabet (0-9, A-H, J-K, M-N, P-T, V-Z). This alphabet is case-insensitive, excludes visually ambiguous characters (I/L/O/U), and is URL-safe.
The first 10 characters encode the timestamp. The last 16 characters encode the randomness. Because the timestamp occupies the most significant position, string comparison (strcmp) produces chronological order.
Monotonic mode
When multiple ULIDs are generated within the same millisecond, the timestamps are identical. If the random portion is generated independently each time, two ULIDs from the same millisecond might sort in arbitrary order.
Monotonic mode fixes this: instead of generating fresh randomness, the generator increments the previous random value by 1. This guarantees that ULIDs within the same millisecond are strictly increasing. When the millisecond changes, fresh randomness is generated again.
The overflow risk is negligible: you would need to generate 2^80 (about 1.2 x 10^24) ULIDs in a single millisecond to exhaust the random counter. No real system approaches this.
Monotonic mode requires the generator to maintain state (the last timestamp and last random value). In multi-threaded applications, this state must be protected by a mutex or atomic operations. If two threads read the same last-random value simultaneously and both increment it, they produce the same ULID. Use a thread-safe ULID library or one generator per thread.
Python example
Because ULID is 128 bits, it fits directly into a UUID column in PostgreSQL, MySQL, or any database with native UUID support. You get time-sorted inserts with zero schema changes compared to UUID v4.
When ULID fits best
ULID is the right choice when you want UUID-compatible IDs that sort by time, need millisecond precision, and prefer no node coordination. It works across services, regions, and languages with nothing more than a clock and a random number generator. The 26-character string representation is human-readable, URL-safe, and copy-paste friendly.
KSUID was created by Segment (now part of Twilio) for a specific need: they wanted time-sorted IDs with the strongest possible collision resistance and did not need millisecond precision. The result is a 160-bit identifier that dedicates 128 bits to randomness — the same entropy as UUID v4 — while still maintaining time-based sorting.
Structure
A KSUID is 20 bytes (160 bits) divided into two parts:
- Timestamp (32 bits) — seconds since a custom epoch of 1,400,000,000 Unix seconds (May 13, 2014, 16:53:20 UTC). With 32 bits of seconds, KSUIDs cover a range of 2^32 seconds, which is roughly 136 years from the epoch (until approximately the year 2150).
- Random payload (128 bits) — cryptographically random bytes, generated fresh for each KSUID. This is the same amount of randomness as a UUID v4, giving 3.4 x 10^38 possible values per second.
The 20 bytes are encoded as a fixed-length 27-character Base62 string using digits 0-9, uppercase A-Z, and lowercase a-z. Because the timestamp occupies the most significant bytes and Base62 preserves byte order, lexicographic string comparison produces chronological order at second granularity.
K-sortable, not strictly monotonic
The "K" in KSUID stands for "K-sortable." Within the same second, multiple KSUIDs have identical timestamps, so their sort order is determined by the random payload. This means two KSUIDs generated in the same second will sort correctly relative to KSUIDs from different seconds, but their relative order within that second is arbitrary.
This is perfectly acceptable for use cases like logging, tracing, and analytics where second-level granularity is sufficient. For workloads that need sub-second ordering, ULID's monotonic mode provides millisecond precision.
Python example
Why KSUID excels for cross-partition ordering
In systems with many independent partitions (microservices, Kafka topics, regional databases), you often need to merge events from different sources into a single timeline. KSUID handles this well because its 128-bit random payload makes cross-partition collisions virtually impossible, and the timestamp prefix provides automatic time ordering when you merge and sort.
The trade-off is that KSUID is the largest of the three formats at 20 bytes (versus 16 for ULID and 8 for Snowflake). In practice, the 4-byte difference from ULID rarely matters, but the 12-byte difference from Snowflake can be significant for tables with billions of rows where index size affects buffer pool utilization.
All three formats solve the same core problem — generating unique, time-sortable identifiers without per-ID coordination — but they make different trade-offs across five dimensions.
Size and storage
Snowflake IDs are 64-bit integers stored as BIGINT. An 8-byte primary key produces the smallest possible B-tree index. For a table with 1 billion rows, the primary key index on Snowflake IDs is roughly 8 GB, compared to 16 GB for ULID and 20 GB for KSUID. This difference determines whether the index fits in your database buffer pool, which directly affects read and write latency.
ULID's 128-bit size matches UUID, so it fits natively into UUID column types in PostgreSQL and MySQL without schema changes. KSUID at 160 bits must be stored as CHAR(27), BINARY(20), or a custom type.
Time precision and sort guarantees
Snowflake provides millisecond precision and strict monotonicity per node via the sequence counter. IDs from the same node are guaranteed to increase. IDs from different nodes in the same millisecond are k-sorted (nearly ordered but not strictly).
ULID provides millisecond precision. In monotonic mode, it guarantees strict ordering within the same millisecond on the same generator. Without monotonic mode, same-millisecond ULIDs sort by their random suffix.
KSUID provides second precision only. Within the same second, sort order is determined by the random payload and has no relationship to creation order. This is acceptable for logging, analytics, and tracing but insufficient for workloads requiring sub-second event ordering.
Collision resistance
Snowflake has deterministic uniqueness — if every generator has a distinct machine ID and clocks behave correctly, collisions are impossible by construction. The risk comes from operational failures: duplicate machine IDs or clock regression.
ULID has probabilistic uniqueness with 80 bits of randomness per millisecond. The birthday paradox gives a 50% collision probability after generating roughly 2^40 (about 1 trillion) ULIDs in a single millisecond. At realistic generation rates, collision probability is effectively zero.
KSUID has probabilistic uniqueness with 128 bits of randomness per second — the same entropy as UUID v4. The birthday threshold is 2^64 (about 1.8 x 10^19) KSUIDs per second, which is far beyond any practical workload.
Operational complexity
Snowflake requires one-time coordination to assign unique machine IDs and ongoing monitoring for clock skew. The generator itself is stateful (tracks last timestamp and sequence).
ULID in monotonic mode requires per-generator state (last timestamp and last random value) and thread-safe access. Without monotonic mode, ULID generation is fully stateless.
KSUID generation is fully stateless: read the clock, generate 128 random bytes, concatenate and encode. No coordination, no state, no thread safety concerns.
When to choose each format
Pick Snowflake when you need compact numeric IDs for SQL primary keys and can manage node ID assignment. Best for high-throughput transactional databases where index size matters and you have infrastructure to coordinate machine IDs (ZooKeeper, etcd, Kubernetes pod ordinals).
Pick ULID when you want UUID-compatible IDs that sort by time with millisecond precision. Best for applications migrating from UUID v4, event sourcing systems needing fine-grained ordering, and environments where you want zero coordination between generators.
Pick KSUID when you want maximum collision resistance with minimal operational complexity. Best for distributed logging, tracing, and analytics where second-level precision is sufficient and the simplicity of stateless generation outweighs the larger storage footprint.
In a system design interview, frame your ID format choice around the specific requirements. If the interviewer emphasizes database performance, lead with Snowflake and its 8-byte index advantage. If they emphasize cross-service compatibility, lead with ULID and its UUID drop-in property. If they emphasize simplicity and collision safety, lead with KSUID. Then mention what you are trading away with your choice.
All three formats dramatically outperform random UUID v4 for database write throughput, event ordering, and debugging. The choice between them is a second-order optimization based on your specific constraints around ID size, time precision, collision guarantees, and operational overhead.