{"id":"unique-ids","title":"Unique ID generation","sections":[{"id":"overview","title":"Overview","html":"<p>Every row, message, order and event needs an identifier. On a single database you let it hand out auto-increment numbers and never think about it again. Once you <a href=\"/learn/database#sharding\">shard</a>, run several regions, or create records before they reach a database (on a phone, in a queue consumer), you need another way to make IDs that never collide, without making one machine a bottleneck.</p>\n<p>In interviews this shows up as \"design a unique ID generator\" and, more often, inside other designs: the short code in a <a href=\"/learn/design-pastebin#step-3-design-core-components\">URL shortener or Pastebin</a>, message IDs in a <a href=\"/learn/design-chat-system\">chat system</a>, tweet IDs in a timeline. The interviewer wants to see that you know the options and what each one costs.</p>","headings":[]},{"id":"what-a-good-id-needs","title":"What a good ID needs","html":"<h3 id=\"what-a-good-id-needs\">What a good ID needs</h3>\n<p>Start by asking which properties matter. They pull against each other.</p>\n<ul>\n<li><strong>Unique.</strong> Across every shard, region and restart, forever. This is the only property that's never negotiable.</li>\n<li><strong>Sortable by time.</strong> If IDs roughly follow creation order, you can paginate with <code>WHERE id &#x3C; ?</code> and sort without a separate timestamp. New rows also land at the right-hand end of a B-tree index, which keeps inserts cheap. Across machines, \"roughly\" is usually the best you can get, and that's normally fine. Twitter's term for this was <strong>k-sorted</strong>: in order to within some bound.</li>\n<li><strong>Compact.</strong> 64 bits fits in a <code>BIGINT</code>, a CPU register and most language integer types. 128 bits doubles the size of every primary key and every index entry that refers to it. The text form matters too, if IDs appear in URLs.</li>\n<li><strong>No coordination on the hot path.</strong> If every ID needs a call to a central service, that service is a single point of failure and a throughput ceiling.</li>\n<li><strong>Sometimes, unguessable.</strong> Sequential IDs in public URLs let anyone count your orders or walk through records one by one. That conflicts with being sortable and compact.</li>\n</ul>","headings":[{"id":"what-a-good-id-needs","title":"What a good ID needs","depth":3}]},{"id":"auto-increment-ticket-servers-and-ranges","title":"Auto-increment, ticket servers and ranges","html":"<h3 id=\"auto-increment-ticket-servers-and-ranges\">Auto-increment, ticket servers and ranges</h3>\n<p><strong>Auto-increment</strong> (a sequence in PostgreSQL, <code>AUTO_INCREMENT</code> in MySQL) is compact, ordered and free. Its limits show up at scale:</p>\n<ul>\n<li>Every insert goes through one database, which caps write throughput.</li>\n<li>Two shards each counting from 1 produce colliding IDs, so you can't merge or move data between them without renumbering.</li>\n<li>Values aren't gapless. Rolled-back transactions and crashes skip numbers, so never treat the IDs as a count.</li>\n<li>They reveal volume and are easy to enumerate if exposed publicly.</li>\n</ul>\n<p><strong>Multi-master with a step.</strong> Give N database servers the same increment and different starting offsets. In MySQL, <code>auto_increment_increment</code> and <code>auto_increment_offset</code> do this: with two servers, one makes 1, 3, 5… and the other 2, 4, 6…. Collisions are gone, but adding a third server means changing the step everywhere, and the IDs aren't time-ordered across servers, because one server can run far ahead of the other.</p>\n<p><strong>Ticket servers.</strong> Flickr published a version of this in 2010. A dedicated MySQL instance does nothing but hand out numbers: each request does a <code>REPLACE INTO</code> on a one-row table and reads <code>LAST_INSERT_ID()</code>. Two ticket servers, one producing odd numbers and one even, remove the single point of failure. It's simple and works well up to the throughput of a database, but every ID is still a network call.</p>\n<figure class=\"diagram\" data-text=\"App server -> Ticket server A 1, 3, 5, …; App server -> Ticket server B 2, 4, 6, …; App server -> Ticket server A 1, 3, 5, …; App server -> Ticket server B 2, 4, 6, …\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 480 277\" width=\"480\" role=\"img\" aria-label=\"Two ticket servers with a step of 2. Either one can serve every app server.\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"42\" x2=\"327\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"125\" y1=\"69\" x2=\"340\" y2=\"198\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"125\" y1=\"207\" x2=\"340\" y2=\"78\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"151\" y1=\"234\" x2=\"327\" y2=\"234\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-box\" x=\"12\" y=\"18\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"46.5\">App server</tspan></text><rect class=\"dg-node dg-box\" x=\"12\" y=\"210\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"238.5\">App server</tspan></text><path class=\"dg-node dg-store\" d=\"M332,18 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M332,18 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"41.5\">Ticket server A</tspan><tspan x=\"400\" y=\"57.5\">1, 3, 5, …</tspan></text><path class=\"dg-node dg-store\" d=\"M332,210 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M332,210 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"233.5\">Ticket server B</tspan><tspan x=\"400\" y=\"249.5\">2, 4, 6, …</tspan></text></svg></div><figcaption>Two ticket servers with a step of 2. Either one can serve every app server.</figcaption></figure>\n<p><strong>Range allocation.</strong> Instead of asking for one ID at a time, each application server reserves a block, say 1,000 IDs, with a single atomic update (<code>UPDATE counters SET next = next + 1000 … RETURNING next</code>) and hands them out from memory. Database load drops by a factor of 1,000, and a short database outage doesn't stop ID generation until the current block runs out. The cost: a crashed server leaves a gap of unused IDs, and IDs from different servers interleave out of time order.</p>","headings":[{"id":"auto-increment-ticket-servers-and-ranges","title":"Auto-increment, ticket servers and ranges","depth":3}]},{"id":"uuids-v4-and-v7","title":"UUIDs: v4 and v7","html":"<h3 id=\"uuids-v4-and-v7\">UUIDs: v4 and v7</h3>\n<p>UUIDs are 128-bit identifiers that any machine can make without asking anyone. The text form is 36 characters: 32 hex digits and 4 hyphens. RFC 9562 (2024) replaced the original RFC 4122 and added new versions, including version 7.</p>\n<p><strong>UUID v4</strong> is 122 random bits; the other 6 bits mark the version and variant. Collisions are possible in theory and irrelevant in practice when the random source is good. By the birthday bound, you'd need about √(2 ln 2 × 2^122) ≈ 2.7 × 10^18 UUIDs for a 50% chance of a single duplicate. At a billion a second, that takes about 86 years. Real collisions come from bad randomness, such as cloned virtual machines with identical random state or weak generators, not from the math.</p>\n<p>The problem with v4 is databases. Random keys land all over the B-tree index, so every insert touches a random page. Once the index is bigger than memory, inserts cause disk reads and frequent page splits. In MySQL's InnoDB, rows are stored in primary-key order, and every secondary index stores the primary key too, so random 16-byte keys make the whole table slower and larger.</p>\n<p><strong>UUID v7</strong> fixes that. Its first 48 bits are a Unix timestamp in milliseconds, followed by the version and variant bits and 74 bits that are random (the RFC also allows using some of them as a counter to keep IDs from one generator in strict order). IDs sort by creation time both as bytes and as text, so inserts append to the end of the index, as with auto-increment, while still needing no coordination. Some databases now generate it natively; PostgreSQL 18 added a <code>uuidv7()</code> function. The trade-off: anyone holding the ID can read its creation time.</p>\n<p>Other versions exist. v1 uses a timestamp and the machine's MAC address, which leaks the address and doesn't sort well; v6 reorders v1's fields to sort properly. Unless you have a reason, use v7 for new keys and v4 when you specifically want no timing information.</p>\n<p>One warning applies to every version: RFC 9562 says implementations should not assume UUIDs are hard to guess. For password-reset links or API secrets, use a dedicated random token of 128 bits or more from a cryptographic generator.</p>","headings":[{"id":"uuids-v4-and-v7","title":"UUIDs: v4 and v7","depth":3}]},{"id":"snowflake-style-ids","title":"Snowflake-style IDs","html":"<h3 id=\"snowflake-style-ids\">Snowflake-style IDs</h3>\n<p>Twitter built Snowflake in 2010 while moving from MySQL to Cassandra, which has no auto-increment. It packs a timestamp, a machine number and a counter into one 64-bit integer:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span> 1 bit  41 bits                          10 bits      12 bits</span></span>\n<span class=\"line\"><span>+-----+--------------------------------+------------+------------+</span></span>\n<span class=\"line\"><span>|  0  | milliseconds since your epoch  | machine ID | sequence   |</span></span>\n<span class=\"line\"><span>+-----+--------------------------------+------------+------------+</span></span></code></pre>\n<ul>\n<li><strong>41 bits of milliseconds</strong> cover 2^41 ms ≈ 2.2 × 10^9 seconds ≈ 69.7 years from a custom epoch you choose. Counting from 1970 would waste decades of range, so pick a recent date. Twitter's epoch is in November 2010. Discord uses the same idea with an epoch of 1 January 2015.</li>\n<li><strong>10 bits of machine ID</strong> allow 1,024 generators. Twitter split them into 5 bits of data center and 5 bits of worker.</li>\n<li><strong>12 bits of sequence</strong> allow 4,096 IDs per millisecond per machine, about 4 million a second.</li>\n<li>The top bit stays 0 so the ID is positive in languages with signed 64-bit integers.</li>\n</ul>\n<p>A generator needs only a few lines of code:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading, time</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">EPOCH_MS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1_704_067_200_000</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  # 2024-01-01T00:00:00Z. Pick once, never change.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Snowflake</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __init__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, machine_id: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        assert</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x3C;=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> machine_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1024</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.machine_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> machine_id</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_ms </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.seq </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.lock </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading.Lock()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> next_id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.lock:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(time.time() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_ms:            </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># clock stepped backwards:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_ms            </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># keep counting in the last millisecond</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_ms:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.seq </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.seq </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> 0x</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">FFF</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.seq </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:             </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># 4,096 IDs used up this millisecond</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    while</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_ms:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(time.time() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.seq </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_ms </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> EPOCH_MS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 22</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.machine_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 12</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.seq</span></span></code></pre>\n<p>The interesting parts are the failure cases.</p>\n<ul>\n<li><strong>Sequence overflow.</strong> If a machine needs more than 4,096 IDs in one millisecond, it waits for the next millisecond. That caps throughput per machine, which is why you'd run several generators.</li>\n<li><strong>Clocks going backwards.</strong> NTP can step a clock back, and a virtual machine's clock can jump after migration. If the generator trusted the clock blindly, it could repeat a timestamp and sequence it had already used. The options are to refuse to generate until the clock catches up (Twitter's original behavior), to keep counting from the last timestamp as the code above does (which stalls the same way if the jump is large), or to fail loudly and take the node out of service. Configuring time sync to slew the clock gradually instead of stepping it avoids most of this.</li>\n<li><strong>Machine ID assignment.</strong> Two processes with the same machine ID will produce duplicates, so assignment has to be reliable. Options include static configuration, a lease from ZooKeeper or etcd, or a stable ordinal such as a Kubernetes StatefulSet index. A lease has to expire before the ID can be reused.</li>\n<li><strong>JavaScript.</strong> JavaScript numbers lose precision above 2^53, and Snowflake IDs are larger. That's why Twitter's API added string <code>id_str</code> fields and Discord sends IDs as strings in JSON.</li>\n<li><strong>Low bits under light load.</strong> When traffic is light, most IDs are the first in their millisecond, so the sequence bits are usually 0. If you shard by <code>id % 16</code>, nearly everything lands on shard 0. Hash the ID before taking the modulus, or shard on something else.</li>\n</ul>\n<p>Instagram used a variant in 2012: 41 bits of time, 13 bits of logical shard ID and 10 bits of sequence, generated inside PostgreSQL by a stored function. Putting the shard ID in the ID means you can find a row's shard from its ID alone.</p>","headings":[{"id":"snowflake-style-ids","title":"Snowflake-style IDs","depth":3}]},{"id":"ulid-and-other-formats","title":"ULID and other formats","html":"<h3 id=\"ulid-and-other-formats\">ULID and other formats</h3>\n<p><strong>ULID</strong> is a community specification, not an IETF standard. It's 128 bits: a 48-bit millisecond timestamp followed by 80 random bits, written as 26 characters in Crockford's base32 (which avoids the easily confused letters I, L, O and U). ULIDs sort by time as text. The spec's monotonic mode increments the random part when two IDs are created in the same millisecond, so IDs from one generator stay in order.</p>\n<p>ULID and UUID v7 have nearly the same layout, and both fit in a 16-byte column. Choose v7 if your database, language and tooling already handle UUIDs. Choose ULID if a shorter, URL-friendly string matters and you're happy to depend on a library.</p>\n<p>A few others you might meet:</p>\n<ul>\n<li><strong>KSUID</strong> (from Segment): 160 bits, a 32-bit timestamp in seconds plus 128 random bits, written as 27 base62 characters. Coarser time, more randomness.</li>\n<li><strong>MongoDB ObjectId</strong>: 12 bytes, made of a 4-byte timestamp in seconds, a 5-byte random value per process and a 3-byte counter.</li>\n<li><strong>Short public codes</strong>: a 64-bit ID encoded in base62 takes at most 11 characters, which suits URLs. The primer's <a href=\"/learn/design-pastebin#step-3-design-core-components\">Pastebin design</a> uses Base62 for its short links too, though it encodes a hash rather than an ID.</li>\n</ul>","headings":[{"id":"ulid-and-other-formats","title":"ULID and other formats","depth":3}]},{"id":"choosing-between-them","title":"Choosing between them","html":"<h3 id=\"choosing-between-them\">Choosing between them</h3>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th>Scheme</th><th>Size</th><th>Sorted by time?</th><th>Coordination</th><th>Main risk</th></tr></thead><tbody><tr><td>Auto-increment</td><td>64 bits</td><td>yes, on one node</td><td>one database</td><td>write bottleneck; enumerable</td></tr><tr><td>Ticket server or ranges</td><td>64 bits</td><td>roughly</td><td>central service (rarely, with ranges)</td><td>that service's availability; gaps</td></tr><tr><td>UUID v4</td><td>128 bits</td><td>no</td><td>none</td><td>index locality; size</td></tr><tr><td>UUID v7</td><td>128 bits</td><td>yes, to the ms</td><td>none</td><td>reveals creation time</td></tr><tr><td>Snowflake</td><td>64 bits</td><td>yes, to the ms</td><td>assigning machine IDs</td><td>clock problems; 69-year range</td></tr><tr><td>ULID</td><td>128 bits</td><td>yes, to the ms</td><td>none</td><td>non-standard; needs a library</td></tr></tbody></table></div>\n<p>A sensible way to decide:</p>\n<ul>\n<li><strong>One database, internal IDs:</strong> auto-increment is fine. If IDs are shown publicly, add a separate public ID rather than exposing the counter.</li>\n<li><strong>Many writers, and 128-bit keys are acceptable:</strong> UUID v7. Nothing to deploy, and it's friendly to indexes.</li>\n<li><strong>High volume where 64 bits matter</strong> (messages, events, existing <code>BIGINT</code> columns): Snowflake-style, and budget time for machine ID assignment and clock handling.</li>\n<li><strong>Secrets and unguessable links:</strong> a cryptographically random token, not any of the above.</li>\n</ul>\n<p>Whatever you choose, remember that time-based IDs only give <strong>rough</strong> order across machines, because clocks differ by milliseconds. If you need strict order within a scope, such as messages in one conversation, assign a per-scope sequence number at the single place that owns that scope.</p>","headings":[{"id":"choosing-between-them","title":"Choosing between them","depth":3}]},{"id":"check-yourself","title":"Check yourself","html":"<h3 id=\"check-yourself\">Check yourself</h3>\n<details>\n<summary>You shard a MySQL users table across 4 servers, each using AUTO_INCREMENT from 1. What breaks, and what are two ways to fix it?</summary>\n<p>Each shard creates user 1, 2, 3…, so IDs collide across shards. Any global lookup by ID becomes ambiguous, and you can't move a user to another shard without renumbering them. Set increment 4 and offsets 1–4 per shard, or move ID generation out of the shards entirely with Snowflake-style IDs or UUID v7, which also lets you add shards later without reconfiguring.</p>\n</details>\n<details>\n<summary>A Snowflake service uses a 41-bit timestamp from an epoch of 1 January 2024. When does it run out, and what would choosing the Unix epoch have cost?</summary>\n<p>2^41 ms ≈ 69.7 years, so it runs out around September 2093. Counting from 1970 would have used up 54 of those years before the service ever ran, leaving it to overflow around 2039.</p>\n</details>\n<details>\n<summary>Your team shards a table with shard = snowflake_id % 16, and shard 0 gets far more rows than the others. Why?</summary>\n<p><code>id % 16</code> keeps only the lowest 4 bits, which belong to the sequence counter. At low or moderate traffic most IDs are the first in their millisecond, so the sequence is 0 and those bits are 0. Hash the ID before taking the modulus, or shard by something with evenly spread values, such as a user ID's hash.</p>\n</details>\n<details>\n<summary>A PostgreSQL table with a UUID v4 primary key has slowed down on inserts as it has grown past available memory. What's happening, and what would you change?</summary>\n<p>Random keys put each insert on a random page of the primary key index. Once the index no longer fits in memory, many inserts need a disk read first, and pages split often. Switching new rows to UUID v7 makes inserts land next to each other in one narrow, advancing region of the index, which stays in memory. (With old v4 keys spread across the whole range, that region isn't the right-hand edge, but the effect on caching is the same.) Existing rows can keep their v4 IDs, since both fit the same column type.</p>\n</details>\n<details>\n<summary>A chat app uses Snowflake IDs for messages and sorts each conversation by ID. Two users reply at almost the same moment, through different servers. Can their messages appear in the wrong order?</summary>\n<p>Yes. The IDs come from different machines whose clocks can differ by a few milliseconds, so the later message can get the smaller ID. Usually that's acceptable. If you need a strict order that everyone agrees on, have the service that owns the conversation assign a per-conversation sequence number as it accepts each message.</p>\n</details>\n<h3 id=\"further-reading\">Further reading</h3>\n<ul>\n<li><a href=\"https://www.rfc-editor.org/rfc/rfc9562\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9562: Universally Unique IDentifiers (UUIDs)</a>: the current UUID standard, including v7 and its security notes.</li>\n<li><a href=\"https://github.com/twitter-archive/snowflake/tree/snowflake-2010\" target=\"_blank\" rel=\"noopener noreferrer\">Twitter's Snowflake (2010 version)</a>: the original requirements and bit layout in the README, with the Scala source.</li>\n<li><a href=\"https://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/\" target=\"_blank\" rel=\"noopener noreferrer\">Flickr, \"Ticket Servers: Distributed Unique Primary Keys on the Cheap\"</a>: the odd/even ticket server design.</li>\n<li><a href=\"https://discord.com/developers/docs/reference\" target=\"_blank\" rel=\"noopener noreferrer\">Discord API reference: Snowflakes</a>: a production Snowflake layout and why IDs are sent as strings.</li>\n<li><a href=\"https://github.com/ulid/spec\" target=\"_blank\" rel=\"noopener noreferrer\">ULID specification</a>: layout, encoding and monotonic mode.</li>\n<li><a href=\"https://github.com/segmentio/ksuid\" target=\"_blank\" rel=\"noopener noreferrer\">KSUID</a>: Segment's format and the reasoning behind it.</li>\n<li>Instagram Engineering, \"Sharding &#x26; IDs at Instagram\" (2012): the shard-aware variant generated in PostgreSQL.</li>\n<li>Alex Xu, <em>System Design Interview – An Insider's Guide</em> (2020), \"Design a unique ID generator in distributed systems\".</li>\n</ul>","headings":[{"id":"check-yourself","title":"Check yourself","depth":3},{"id":"further-reading","title":"Further reading","depth":3}]}],"sourceFiles":[]}