<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Shaswat Rungta]]></title><description><![CDATA[Personal substack to keep notes about things I am working on. Hope you find some of them useful for what you are doing as well. :)]]></description><link>https://shaswatrungta.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!TAxM!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8215d18c-3112-4f73-be90-58bbe7809783_1280x1280.png</url><title>Shaswat Rungta</title><link>https://shaswatrungta.substack.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 29 Jul 2026 00:36:36 GMT</lastBuildDate><atom:link href="https://shaswatrungta.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Shaswat Rungta]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[shaswatrungta@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[shaswatrungta@substack.com]]></itunes:email><itunes:name><![CDATA[Shaswat Rungta]]></itunes:name></itunes:owner><itunes:author><![CDATA[Shaswat Rungta]]></itunes:author><googleplay:owner><![CDATA[shaswatrungta@substack.com]]></googleplay:owner><googleplay:email><![CDATA[shaswatrungta@substack.com]]></googleplay:email><googleplay:author><![CDATA[Shaswat Rungta]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Measuring LLM Capability and Reliability: Pass@k vs Pass^k]]></title><description><![CDATA[Two similar-looking metrics answer very different questions about model performance.]]></description><link>https://shaswatrungta.substack.com/p/measuring-llm-capability-and-reliability</link><guid isPermaLink="false">https://shaswatrungta.substack.com/p/measuring-llm-capability-and-reliability</guid><dc:creator><![CDATA[Shaswat Rungta]]></dc:creator><pubDate>Wed, 22 Jul 2026 18:30:25 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!NuYc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NuYc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NuYc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png 424w, https://substackcdn.com/image/fetch/$s_!NuYc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png 848w, https://substackcdn.com/image/fetch/$s_!NuYc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png 1272w, https://substackcdn.com/image/fetch/$s_!NuYc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NuYc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png" width="1200" height="630" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:630,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:48410,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/208085480?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NuYc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png 424w, https://substackcdn.com/image/fetch/$s_!NuYc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png 848w, https://substackcdn.com/image/fetch/$s_!NuYc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png 1272w, https://substackcdn.com/image/fetch/$s_!NuYc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ce48272-aa84-4625-b5e5-0bdf6e9ab48a_1200x630.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>The truth about working with LLMs</strong></h2><p>Ask an LLM the same question several times and you may get several different answers. One response might be correct, another subtly wrong, and a third unusable. That variation is a normal part of sampling from a language model.</p><p>This might be okay when you are getting started. But at some point , you must start asking the hard questions. <mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">How much can you trust your model? Will your model actually get to an answer? Can you reliably run the agent automation and expect it to the right thing?</mark></p><p>All these questions imply the need of a mental model to check how good is your model/agent performace. This bring us to <code>pass@k</code> and <code>pass&#710;k</code> scores.</p><p>In its simplest form, for an agent that repeats a task <code>k</code> times, <strong>pass@k</strong> asks: Did at least one of the <code>k</code> attempts succeed?<br><strong>pass^k</strong> asks: Did every one of the <code>k</code> attempts succeed?</p><blockquote><p>At <code>k = 1</code>, they are the same measurement. As <code>k</code> grows, they reveal different properties.</p></blockquote><ul><li><p><code>pass@k</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> measures </mark><strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">capability</mark></strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">. Can repeated sampling uncover a correct answer?</mark></p></li><li><p><code>pass^k</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> measures </mark><strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">reliability</mark></strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">. Can the model produce a correct answer consistently?</mark><br></p></li></ul><p><strong><mark data-color="#f4cccc" style="background-color: rgb(244, 204, 204); color: rgb(0, 0, 0);">&#128680; Super important point &#128680;</mark></strong></p><p><mark data-color="#f4cccc" style="background-color: rgb(244, 204, 204); color: rgb(0, 0, 0);">Pass@K and Pass&#710;K must </mark><strong><mark data-color="#f4cccc" style="background-color: rgb(244, 204, 204); color: rgb(0, 0, 0);">NOT</mark></strong><mark data-color="#f4cccc" style="background-color: rgb(244, 204, 204); color: rgb(0, 0, 0);"> be the only metrics you use for evals.<br>These are only introductory heuristics.</mark></p><h2><strong>Lets take an example.</strong></h2><p>Lets say you gave a coding model some problem to solve.<br>The first answer you got fails the tests. The second one does not compile. The third one handles the happy path but forgets that empty arrays exist. The fourth answer works.</p><p>Will you say that the model successfully completed the task?<br>If you are exploring what the model <strong>can</strong> do, the answer is yes. It found a valid solution. However if this model is running an unattended production workflow, the answer is a nervous no. Three out of four runs failed is not a production friendly metric. &#129397;</p><p><code>Pass </code><span>metrcis answer these two differnt questions. In our case of 4 answers,</span></p><p><code>pass@4</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> records the one working answer and says the model demonstrated the capability.<br></mark><code>pass^4</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> records the three failures and says the model was not reliable across all four attempts.</mark></p><h2><code>pass@k</code> measures the probability that <strong>at least one</strong> of <code>k</code> attempts is correct.<br>For k = 1 (<code>pass@1</code>), the model gets one attempt. Either it works or it does not.<br>For k = 10 (<code>pass@10</code>), the model gets ten attempts. Nine can fail spectacularly. If one passes, the task counts as a success.</h2><p>This may sounf very simple but is quite useful. It tells you whether the model has the solution somewhere inside its distribution.<mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> If you have tests, a verifier, a human reviewer, or another system that can select the good answer, then generating several candidates is a legitimate way to get output.</mark></p><p>It is also why <code>pass@k</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> rises as </mark><code>k</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> grows</mark>. More attempts create more opportunities to get lucky. A models chance of getting an answer right once across 1000 attempts is obviously greater than 10 attempts.</p><p>Mathematically, it is easy to express this score.<br>Assume one attempt has probability <code>p</code> of succeeding and each attempt is independent.<br>The chance that any given attempt fails is <code>(1 - p)</code><br>The chance that all <code>k</code> attempts fail is <code>(1 - p)^k</code>, so<br>The chance that at least 1 attempt passed becomes 1 - (1 - p)^k</p><pre><code><code>pass@k = 1 - (1 - p)^k  
</code></code></pre><p>If a model succeeds 70% of the time:</p><pre><code><code>pass@1 = 70%  
pass@3 = 1 - (1 - 0.70)^3 = 97.3%  
</code></code></pre><p>That looks excellent. Give the model three attempts and it will produce at least one correct answer 97.3% of the time.</p><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">&#9888;&#65039; Someone still has to identify which answer is correct.</mark></p><blockquote><p><code>pass@k</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> quietly assumes there is a selection mechanism after generation. A test suite can do that for code. A symbolic checker can do it for maths. A human can do it for a draft. Without a reliable judge, ten answers may just give you ten confidently written things to inspect.</mark></p></blockquote><h2><strong>Pass^k</strong></h2><p><code>pass^k</code> asks the opposite question: what is the probability that <strong>all</strong> <code>k</code> attempts are correct?</p><p>Under the same independent-attempt assumption:</p><pre><code><code>pass^k = p^k  
</code></code></pre><p>For the same model with a 70% single-attempt success rate:</p><pre><code><code>pass^1 = 70%  
pass^3 = 0.70^3 = 34.3%  
</code></code></pre><p>Same model. Same task. Same three attempts.</p><ul><li><p><code>pass@3</code> says <strong>97.3%</strong></p></li><li><p><code>pass^3</code> says <strong>34.3%</strong></p></li></ul><p>That is not a rounding error. That is a completely different story.</p><p><code>pass@3</code> tells us the model is highly capable when retries and selection are available.<br><code>pass^3</code> tells us that only about one-third of three-run groups are flawless. If every run can send<br>an email, modify a database, approve a refund, or merge code, that distinction matters a lot.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8ey9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8ey9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png 424w, https://substackcdn.com/image/fetch/$s_!8ey9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png 848w, https://substackcdn.com/image/fetch/$s_!8ey9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png 1272w, https://substackcdn.com/image/fetch/$s_!8ey9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8ey9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png" width="1456" height="1384" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1384,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:147406,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/208085480?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8ey9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png 424w, https://substackcdn.com/image/fetch/$s_!8ey9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png 848w, https://substackcdn.com/image/fetch/$s_!8ey9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png 1272w, https://substackcdn.com/image/fetch/$s_!8ey9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5516ba59-9265-4990-8038-d444c6f0711e_1504x1430.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Correlation between the two metrics</strong></h2><p>For an imperfect model, increasing <code>k</code> makes the metrics separate:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Gx-s!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Gx-s!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png 424w, https://substackcdn.com/image/fetch/$s_!Gx-s!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png 848w, https://substackcdn.com/image/fetch/$s_!Gx-s!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png 1272w, https://substackcdn.com/image/fetch/$s_!Gx-s!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Gx-s!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png" width="1456" height="222" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1b697db6-a236-4b87-b266-51f42155293a_1518x231.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:222,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:53619,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/208085480?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Gx-s!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png 424w, https://substackcdn.com/image/fetch/$s_!Gx-s!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png 848w, https://substackcdn.com/image/fetch/$s_!Gx-s!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png 1272w, https://substackcdn.com/image/fetch/$s_!Gx-s!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b697db6-a236-4b87-b266-51f42155293a_1518x231.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>For a model with a 70% single-attempt success rate, the separation looks like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YM0s!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YM0s!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png 424w, https://substackcdn.com/image/fetch/$s_!YM0s!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png 848w, https://substackcdn.com/image/fetch/$s_!YM0s!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png 1272w, https://substackcdn.com/image/fetch/$s_!YM0s!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YM0s!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png" width="1456" height="1004" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1004,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:87115,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/208085480?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YM0s!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png 424w, https://substackcdn.com/image/fetch/$s_!YM0s!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png 848w, https://substackcdn.com/image/fetch/$s_!YM0s!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png 1272w, https://substackcdn.com/image/fetch/$s_!YM0s!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69b15d83-9ff5-4bd8-ad36-794c70a32eb7_1510x1041.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>This creates a fun benchmark trick.</p><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">If a model has a 20% chance of solving a difficult problem in one attempt, its </mark><code>pass@1</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> is only 20%. But its theoretical </mark><code>pass@20</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> is about 98.8%.</mark></p><p>The model obvoisly did not suddenly become five times smarter. <mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Search, retries, and verification are real system components. The mistake is presenting </mark><code>pass@20</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> as if it describes the experience of a user who asks once and accepts the first answer.</mark></p><h2><strong>But how do I already know the success rate?</strong></h2><p>The previous formulas assume we already know the model&#8217;s true success probability <code>p</code>. In an evaluation, we do not know it. We generate samples and estimate it.</p><p>Suppose we setup a benchmark that generates:</p><ul><li><p><code>n</code> total answers for one problem</p></li><li><p>out of that <code>c</code> are correct answers</p></li><li><p>we want to evaluate groups of <code>k</code> answers</p></li></ul><p>The standard estimator introduced with <a href="https://arxiv.org/abs/2107.03374">HumanEval</a> and used by its<br><a href="https://github.com/openai/human-eval/blob/master/human_eval/evaluation.py">reference implementation</a> for <code>pass@k</code> is:</p><pre><code><code>pass@k = 1 - C(n - c, k) / C(n, k)  
</code></code></pre><p><code>C(a, b)</code> means &#8220;the number of ways to choose <code>b</code> items from <code>a</code> items.&#8221;</p><blockquote><p>The fraction calculates how many groups of <code>k</code> contain only failures. Subtracting it from 1 gives the fraction containing <strong>at least one success</strong>.</p></blockquote><p>The matching estimator for <code>pass^k</code> is:</p><pre><code><code>pass^k = C(c, k) / C(n, k)  
</code></code></pre><blockquote><p>Here we count only groups where all <code>k</code> selected answers come from the correct answers.</p></blockquote><p>If you are like me and have trouble parsing permutations and combinations, the below example should clarify the maths.<br>For example, imagine we generated 10 answers and 7 out of 10 passed:</p><pre><code><code>n = 10  
c = 7  
k = 3  
  
pass@3 = 1 - C(3, 3) / C(10, 3)  
       = 119 / 120  
       = 99.2%  
  
pass^3 = C(7, 3) / C(10, 3)  
       = 35 / 120  
       = 29.2%  
</code></code></pre><p>The numbers differ slightly from the earlier 97.3% and 34.3% because this calculation operates on the ten samples we actually observed, without replacement. The earlier calculation assumed an underlying 70% success probability and independent attempts.</p><h2><strong>Pass@k may feel inflated but is quite useful</strong></h2><p>It is tempting to look at the gap and declare <code>pass@k</code> a misleading metric.</p><p>However, <code>pass@k</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> is excellent at measuring </mark><strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">coverage</mark></strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">:</mark></p><ul><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Can the model solve this problem at all?</mark></p></li><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Does sampling reveal atleast one correct reasoning path?</mark></p></li><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">If I have a verifier-backed system, can I find a good candidate?</mark></p></li><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Does the model produce diverse solutions rather than repeating one mistake?</mark></p></li></ul><p>For a coding assistant, this can match reality. Generate several implementations, run tests, and show the developer the one that passes. The retries are then part of the product.</p><blockquote><p>&#8220;The model gets 95% on pass@10&#8221; does not mean &#8220;the model is correct 95% of the time.&#8221; It means that, under that benchmark&#8217;s prompt, sampling settings, tests, and candidate count, at least one of ten generated answers passed for 95% of tasks.</p></blockquote><p>That is valuable information.</p><h2><code>pass^k</code> becomes important as we move from <strong>augmentation</strong> to <strong>automation</strong>. This distinction also appears in recent <a href="https://arxiv.org/abs/2602.16666">agent reliability research</a>: We as humans are okay with some inconsistency in augmentation tools. However autonomous systems turn unreliable output directly into unreliable action.</h2><p>With augmentation, a human remains in the loop:</p><ul><li><p>A developer reviews generated code</p></li><li><p>An analyst checks a generated query</p></li><li><p>A writer edits a generated draft</p></li><li><p>An operator approves the proposed action</p></li></ul><p>One bad attempt is inconvenient. The humans are the safeguard.</p><p>With automation, the output becomes the action:</p><ul><li><p>The agent changes production configuration</p></li><li><p>The workflow sends messages to customers</p></li><li><p>The system updates financial records</p></li><li><p>The bot closes incidents or deletes resources</p></li></ul><p>Now one bad attempt can bring down production systems.</p><p>There is also a componding problem. An agent that succeeds nine times out of ten may sound production-ready. But when we run it through a 20-step workflow where every step must succeed, and the probability of a flawless run is:</p><pre><code><code>0.9^20 = 12.2%  
</code></code></pre><p>This is why long agent workflows feel more fragile than their individual demos. Small failure rates compound.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eJld!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eJld!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png 424w, https://substackcdn.com/image/fetch/$s_!eJld!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png 848w, https://substackcdn.com/image/fetch/$s_!eJld!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png 1272w, https://substackcdn.com/image/fetch/$s_!eJld!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eJld!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png" width="1456" height="165" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:165,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:51836,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/208085480?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eJld!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png 424w, https://substackcdn.com/image/fetch/$s_!eJld!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png 848w, https://substackcdn.com/image/fetch/$s_!eJld!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png 1272w, https://substackcdn.com/image/fetch/$s_!eJld!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F515c7242-41cc-4800-948b-bf01453bfc5c_2273x258.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2><strong>What should you report?</strong></h2><p>There is no single perfect number. Report the metric that matches how the system will be used.</p><h3><strong>Report pass@1</strong></h3><p>This is the clean baseline. What happens when the user asks once and accepts one answer?</p><h3><strong>Report pass@k when retries are expected and part of the system</strong></h3><p>Use it when your actual system generates <code>k</code> candidates and has a credible way to select one:</p><ul><li><p>Tests</p></li><li><p>A formal verifier</p></li><li><p>A deterministic constraint checker</p></li><li><p>A human reviewer</p></li><li><p>A separately evaluated reranker</p></li></ul><p>Do not report <code>pass@100</code> for a product that only generates one answer.</p><h3><strong>Report pass^k when consistency matters</strong></h3><p>Use it when repeated runs should remain correct, or when a sequence contains several opportunities for failure. For examples, agent workflow chaining, regression testing, safety-sensitive actions, and workflows without a human backstop.</p><h3><strong>Why not report both!</strong></h3><p>The gap between the metrics is itself useful.<br>At <code>k = 1</code>, they are the same measurement. As <code>k</code> grows, they reveal different properties.</p><ul><li><p>High <code>pass@k</code>, low <code>pass^k</code>: capable but inconsistent</p></li><li><p>Low <code>pass@k</code>, low <code>pass^k</code>: the task is beyond the model</p></li><li><p>High <code>pass@k</code>, high <code>pass^k</code>: capable and dependable</p></li></ul><p>That tells a much richer story than any one of them.</p><h2><strong>A few traps before you calculate either</strong></h2><h3><strong>Attempts are not always independent</strong></h3><p>Models can repeat the same reasoning pattern, especially at low temperature. Agent runs may share the same retrieved documents, tools, cached state, or environmental failure. The simple <code>p</code> formulas are intuition, not a guarantee that real runs behave independently.</p><h3><strong>Correctness is only as good as the judge</strong></h3><p>A code sample &#8220;passes&#8221; because it passed the available tests. Missing tests can turn a wrong answer into a benchmark success. A non reliable evaluator can mess up your evalutions.</p><h3><strong>Sampling settings are part of the metric</strong></h3><p>Temperature, prompts, tool access, token limits, and model versions all change the result. Comparing two <code>pass@k</code> numbers with different evaluation setups is not a clean model comparison.</p><h3><strong>Reliability has more than one dimension</strong></h3><p><code>pass^k</code> captures repeated correctness. It does not measure calibration, safety, robustness to prompt changes, recovery from tool failures, or behavior under a changing environment.</p><h2><strong>Key takeaways</strong></h2><ol><li><p><code>pass@k</code><strong> measures capability</strong> - at least one of <code>k</code> attempts must succeed.</p></li><li><p><code>pass^k</code><strong> measures consistency</strong> - all <code>k</code> attempts must succeed.</p></li><li><p><strong>More retries make </strong><code>pass@k</code><strong> rise</strong> - capability becomes easier to discover.</p></li><li><p><strong>More required successes make </strong><code>pass^k</code><strong> fall</strong> - unreliability compounds.</p></li><li><p><strong>Retries need a judge</strong> - multiple candidates help only if you can identify the good one.</p></li><li><p><strong>Automation raises the bar</strong> - a human can absorb inconsistency; an unattended workflow cannot.</p></li><li><p><strong>Report both when trust matters</strong> - one tells you what the model can do, the other how often you can depend on it.</p></li></ol><h2><strong>Conclusions.</strong></h2><p>If you are building a demo, exploring the edge of model capability, or using a strong verifier, <code>pass@k</code> is exactly the question you want to ask.</p><p>If you are giving an agent tools, permissions, and the ability to act without approval, ask the other question too:</p><blockquote><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">It succeeded once. Will it succeed again?</mark></p></blockquote><h2><strong>References</strong></h2><ul><li><p><a href="https://arxiv.org/abs/2107.03374">Evaluating Large Language Models Trained on Code</a> - the HumanEval paper that introduced the <code>pass@k</code> estimator.</p></li><li><p><a href="https://github.com/openai/human-eval/blob/master/human_eval/evaluation.py">OpenAI HumanEval </a><code>pass@k</code><a href="https://github.com/openai/human-eval/blob/master/human_eval/evaluation.py"> implementation</a> - the reference code for the unbiased estimator used above.</p></li><li><p><a href="https://arxiv.org/abs/2602.16666">Towards a Science of AI Agent Reliability</a> - recent work on why consistency matters as models move from augmentation to automation.</p><div class="pullquote"><p>Originally posted on <a href="https://srungta.github.io/">Shaswat Rungta | Personal Blog</a></p></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://shaswatrungta.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hope this was a useful read for you! Would you like to subscribe to keep in touch? &#128591;&#127995; &#129782;&#127995; &#129392; </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div></li></ul>]]></content:encoded></item><item><title><![CDATA[When 💩 hits the database]]></title><description><![CDATA[Handling emojis and unicode characters in production.]]></description><link>https://shaswatrungta.substack.com/p/when-hits-the-database</link><guid isPermaLink="false">https://shaswatrungta.substack.com/p/when-hits-the-database</guid><pubDate>Tue, 21 Jul 2026 14:51:30 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!PibK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PibK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PibK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg 424w, https://substackcdn.com/image/fetch/$s_!PibK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg 848w, https://substackcdn.com/image/fetch/$s_!PibK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg 1272w, https://substackcdn.com/image/fetch/$s_!PibK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PibK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg" width="1456" height="764" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/df7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:764,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4138,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/svg+xml&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/207924465?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PibK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg 424w, https://substackcdn.com/image/fetch/$s_!PibK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg 848w, https://substackcdn.com/image/fetch/$s_!PibK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg 1272w, https://substackcdn.com/image/fetch/$s_!PibK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf7d6494-9012-4f8d-82b4-6eb8c68635cb_1200x630.svg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Chapter 1: A brief history of world knowledge</strong></h2><p>The need for communication and passing on knowledge is one of the most essential human traits. We have evolved from sign languages and cave paintings to using leaves and stones for the written word. We sped through the whole paper era (in a short couple of centuries ) and landed in the information age where everything went digital.</p><p>In the beginning, computers had a small vocabulary. We were so busy getting the word out about the internet, we had little time to consider if the word was understood. As the internet grew from a mostly English-speaking network into a place for the whole world, text had to evolve with it. Every step made online communication richer, but it also quietly broke another assumption programmers had made about what a &#8220;character&#8221; really is.</p><blockquote><p>&#129488; Are emojis modern day cave paintings?? &#128559;</p></blockquote><h3><strong>ASCII (The Simple Days)</strong></h3><pre><code><code>A = 65
B = 66
...
Z = 90
</code></code></pre><p>128 characters total. All English letters, numbers, basic punctuation. 7 bits. Life was simple.</p><p>&#127758; <strong>Then the internet happened.</strong></p><h3><strong>Unicode (The Complicated Now)</strong></h3><pre><code><code>A = U+0041 (Latin Capital Letter A)
&#193; = U+00C1 (Latin Capital Letter A with Acute)
&#1040; = U+0410 (Cyrillic Capital Letter A)
&#913; = U+0391 (Greek Capital Letter Alpha)
&#119808; = U+1D400 (Mathematical Bold Capital A)
</code></code></pre><p>140,000+ characters. Multiple ways to represent the same thing. Combining characters. Emoji. Emoji with skin tones. Emoji combined with other emoji.</p><blockquote><p>Unicode went from <strong>&#8220;character set&#8221;</strong> to <strong>&#8220;every written symbol in human history plus emoji&#8221;</strong>.</p></blockquote><h2><strong>Chapter 2: The Many Ways Characters Lie About Their Size</strong></h2><p>Traditional knowledge tells us that the size of a string should be equal to the number of character you see in it. And traditionally all characters used to take a fixed 1 byte memory. So if i wanted to store a <code>Name</code> of 100 characters, I would allocate 100 length to my database column.<br>But unfortunately modern unicode breaks these assumption in many ways, most commonly due to the way the size of a character is calculated.</p><h3><strong>Size Lie #1: UTF-8 Variable Length</strong></h3><p>Single characters in print can often need more space to store.</p><pre><code><code>'A'.length;        // 1 UTF-16 code unit (1 byte in UTF-8)
'&#8364;'.length;        // 1 UTF-16 code unit (3 bytes in UTF-8)
'&#128169;'.length;       // 2 UTF-16 code units (4 bytes in UTF-8)
</code></code></pre><p>If you had a 100-byte limit thinking it should store 100 characters, what fits is actually:</p><ul><li><p>100 ASCII characters &#9989;</p></li><li><p>33 Euro signs (3 bytes each)</p></li><li><p>25 emoji (4 bytes each)</p></li></ul><h3><strong>Size Lie #2: Combined Characters</strong></h3><p>Sometime single characters may not be single characters at all. Depending on how you process them they can be of different lengths,</p><pre><code><code>'&#233;'.length;  // Could be 1 or 2!

// Option 1: Single character (NFC - composed)
'&#233;' = U+00E9 (1 character)

// Option 2: Base + combining accent (NFD - decomposed)  
'e' + '&#769;' = U+0065 + U+0301 (2 characters)
</code></code></pre><p>The characters look identical. But they&#8217;re different in memory. This also leads to an interesting comparison problem.<br><strong>&#8216;&#233;&#8217; === &#8216;&#233;&#8217; can sometimes be false!</strong> Which means your database indexes them as different values and your lookups fail mysteriously.</p><blockquote><p>Usually most languages have an equivalent of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize">normalise()</a> function for strings. In that case <strong>&#8216;&#233;&#8217;.normalize() === &#8216;&#233;&#8217;.normalize()</strong> gives a <strong>True</strong> result.</p></blockquote><h3><strong>Size Lie #3: Emoji Combiners</strong></h3><p>Emojis revolutionised the way humans communicate. However emojis follow an interesting composition pattern.</p><pre><code><code>'&#128104;&#8205;&#128105;&#8205;&#128103;&#8205;&#128102;'.length;  // 11

// Actually:
'&#128104;' + ZWJ + '&#128105;' + ZWJ + '&#128103;' + ZWJ + '&#128102;'
// Man + Zero Width Joiner + Woman + ZWJ + Girl + ZWJ + Boy
// = Family emoji (displays as 1)
</code></code></pre><p>Your JavaScript counter reports &#8220;This is 11 UTF-16 code units,&#8221; while the user sees just 1 emoji. Your validation thinks &#8220;That&#8217;s fine,&#8221; but a byte-limited field can reject it: this sequence is 25 UTF-8 bytes.</p><h2><strong>Chapter 3: Real-World Horror Stories</strong></h2><p>All this information is not just theoritical. It happens in production system day-in day-out. Below are some ways in which they can happen trivially.</p><h3><strong>Story 1: The Name That Broke Authentication</strong></h3><p>A user named <strong>Jos&#233;</strong> creates an account. Your system normalizes and stores it as &#8220;<strong>Jos&#233;</strong>&#8221; using NFC normalization. When <strong>Jos&#233;</strong> logs in the next day and types their name, their keyboard or input method produces the NFD normalized version: &#8220;<strong>Jos&#233;</strong>&#8221;. Your system tries to look up &#8220;<strong>Jos&#233;</strong>&#8221; in the database but finds nothing, because it&#8217;s searching for the NFD form against the stored NFC form. The user sees &#8220;Who&#8217;s <strong>Jos&#233;</strong>? I don&#8217;t know that user.&#8221;</p><blockquote><p><strong>NFD - Normalization Form Decomposed.</strong> Breaks characters into base character + combining marks. Example: &#233; = e + &#180; (separate bytes) <strong>NFC - Normalization Form Composed.</strong> Combines base character + marks into single precomposed character. Example: &#233; = &#233; (single byte)</p></blockquote><p><strong>Fix:</strong> Normalize all input before comparison</p><pre><code><code>function normalizeText(str) {
  return str.normalize('NFC');
}

username === savedUsername  // &#10060; Can fail
normalizeText(username) === normalizeText(savedUsername)  // &#9989; Works
</code></code></pre><h3><strong>Story 2: The Email That Traveled Through Time</strong></h3><p>A User in Japan sends email with kanji characters.<br><strong>Email server 1 (UTF-8):</strong> Passes it along fine<br><strong>Email server 2 (ISO-8859-1):</strong> Can&#8217;t handle kanji, converts to <code>?</code><br><strong>Email server 3 (UTF-8):</strong> Sees <code>?</code> and assumes that&#8217;s the data<br><strong>Recipient:</strong> Gets email full of <code>?</code> characters</p><p><strong>This is mojibake.</strong> Text corrupted by charset mismatches.</p><p><strong>The fix:</strong> Always specify charset in Content-Type headers</p><pre><code><code>Content-Type: text/html; charset=UTF-8
</code></code></pre><blockquote><p>This is also one of the reasons why old smartphones could not render emojis in SMS and showed a string of ? or black boxed question marks.</p></blockquote><h3><strong>Story 3: The Tweet That Was Too Long</strong></h3><p>On Twitter <em>(I refuse to call it X! )</em>, tweets are limited to 280 characters.</p><p><strong>User tweets:</strong> &#8220;&#127988;&#917607;&#917602;&#917605;&#917614;&#917607;&#917631;&#127988;&#917607;&#917602;&#917619;&#917603;&#917620;&#917631;&#127988;&#917607;&#917602;&#917623;&#917612;&#917619;&#917631; (3 flag emoji).<br><strong>Intuition:</strong> &#8220;This is 3 characters&#8221;<br><strong>Reality:</strong> This is 42 code points (14 per flag)</p><p><strong>Result:</strong> The twitter character counter disagrees with you and shows you are using much more than 3 characters.</p><p><strong>Platform reality:</strong> X uses its own weighted counting rules, not JavaScript&#8217;s built-in string length.</p><p>You can read about there actual computation on their <a href="https://docs.x.com/fundamentals/counting-characters">developer site.</a></p><blockquote><p>Grapheme clusters are the user-perceived characters. A single emoji flag (like &#127988;&#917607;&#917602;&#917605;&#917614;&#917607;&#917631;) is one grapheme cluster even though it&#8217;s 14 code points. A code point is Unicode&#8217;s assigned number for a unit of text, such as a letter, emoji component, or invisible joiner.</p></blockquote><h3><strong>Story 4: The Database Migration That Deleted Data</strong></h3><p>When migrating database, your primary focus is that tables should not get dropped, there should be no downtime, the columns should get mapped correctly. However managing migrations of character set has its own set of challengers.</p><p><strong>Scenario:</strong> Migrating from MySQL with <code>latin1</code> to <code>utf8mb4</code></p><pre><code><code>-- Old table
CREATE TABLE users (
  name VARCHAR(100) CHARACTER SET latin1
);

-- New table
CREATE TABLE users (
  name VARCHAR(100) CHARACTER SET utf8mb4
);
</code></code></pre><p><strong>The trap:</strong></p><ul><li><p><code>latin1</code>: <code>VARCHAR(100)</code> stores up to 100 characters, using up to 100 bytes for the value</p></li><li><p><code>utf8mb4</code>: <code>VARCHAR(100)</code> still stores up to 100 characters, using up to 400 bytes for the value</p></li></ul><p><strong>But MySQL also has row and index-size limits, depending on the storage engine, row format, and indexes.</strong></p><p>If you have 200 VARCHAR(100) columns:</p><ul><li><p><code>latin1</code>: 200 &#215; 100 = 20,000 bytes &#9989;</p></li><li><p><code>utf8mb4</code>: 200 &#215; 400 = 80,000 bytes at the theoretical maximum</p></li></ul><p>your migration could hit an engine-specific limit, or your data could get truncated if the migration is not planned carefully. The fix would usually be to reduce VARCHAR sizes or split across tables.</p><h3><strong>Story 5: The Search That Found Nothing</strong></h3><p>Since we are talking databses, searching for an existing value is often the most popular use case. Lets take an example.<br><strong>User searches for:</strong> &#8220;caf&#233;&#8221;<br><strong>Database has:</strong> &#8220;cafe&#8221;, &#8220;caf&#233;&#8221;, &#8220;caf&#233;&#8221; (composed), &#8220;caf&#233;&#8221; (decomposed)</p><p><strong>Your search:</strong></p><pre><code><code>SELECT * FROM places WHERE name = 'caf&#233;';
</code></code></pre><p><strong>Results:</strong> DB would usually return only exact matches and misses variations which in this case you obviously wanted.</p><p><strong>The fix:</strong> Normalize before storing, normalize before searching</p><pre><code><code>-- PostgreSQL example
SELECT * FROM places WHERE unaccent(name) = unaccent('caf&#233;');
</code></code></pre><h3><strong>Story 6: The URL That Broke Routing</strong></h3><p>Unicode also turns up in places where you assume some default behaviour. One of the common ways sites create persistent readable links is to have text in the URL path. However browsers have been built to handle unicode by default when routing.</p><p><strong>User creates profile:</strong> <code>https://site.com/users/Jos&#233;</code>.<br><strong>Browser URL-encodes:</strong> <code>https://site.com/users/Jos%C3%A9</code></p><p><strong>Your router:</strong></p><pre><code><code>app.get('/users/:name', (req, res) =&gt; {
  const user = getUser(req.params.name);
  // req.params.name is "Jos%C3%A9" or "Jos&#233;" depending on framework
});
</code></code></pre><p>Some frameworks would decode automatically but it is always good to check. Anyway, a better way to create permalinks is not to have user input in the URL.</p><p><strong>The fix:</strong> Always use IDs in URLs, not names</p><pre><code><code>// &#10060;  Bad
/users/Jos&#233;

// &#9989; Good
/users/123
</code></code></pre><h2><strong>Chapter 4: The Emoji Special Cases</strong></h2><p>We talk about compositional emojis. However the composition itself has many different variation.</p><h3><strong>Skin Tone Modifiers</strong></h3><p>Emojis have additive modifiers. You take a base emoji and add an inflextion on it. This is how you get a hand wave sign in your own skin tone.</p><pre><code><code>'&#128075;'.length;      // 2 (base emoji)
'&#128075;&#127995;'.length;     // 4 (base emoji + skin tone modifier)
</code></code></pre><p>Your emoji picker now shows 5 skin tone options. But in your database each takes different space. So beware of these additions.</p><h3><strong>Flags</strong></h3><p>Flags are a fun variation of composition. For example, If you look at the US flag emoji.</p><pre><code><code>'&#127482;&#127480;'.length;     // 4

// Actually two "Regional Indicator" characters:
// U+1F1FA (Regional Indicator U) + U+1F1F8 (Regional Indicator S)
// = US flag
</code></code></pre><p>Your database may not recognize this as one flag<br>Your rendering might show &#8220;U&#8221; &#8220;S&#8221; instead of &#127482;&#127480;</p><h3><strong>Gender and Profession Variations</strong></h3><p>Similar variations arise for gender and profession emojis.</p><pre><code><code>'&#128104;'              // Man
'&#128104;&#8205;&#9877;&#65039;'             // Man + ZWJ + Medical symbol = Male doctor
'&#128105;&#8205;&#9877;&#65039;'             // Woman + ZWJ + Medical symbol = Female doctor
'&#129489;&#8205;&#9877;&#65039;'             // Person + ZWJ + Medical symbol = Doctor (gender neutral)
</code></code></pre><p>Each variation has a different byte count</p><h2><strong>Chapter 5: The Database Encoding Trap</strong></h2><p>I have spent years working with databases without caring about caracter sets. In most case, databases come with good defaults, or their onboarding guides give examples of most defensive settings. However it is good to know how they affect storage in the context of what we discussed thus far.</p><h3><strong>MySQL Character Sets (A History of Mistakes)</strong></h3><p><code>latin1</code> (default for ancient MySQL)</p><ul><li><p>1 byte per character</p></li><li><p>Only Western European languages</p></li><li><p>Can&#8217;t store emoji</p></li></ul><p><code>utf8</code> (MySQL&#8217;s version, not real UTF-8)</p><ul><li><p>Max 3 bytes per character</p></li><li><p>Can store most characters</p></li><li><p><strong>Cannot store emoji</strong> (emoji need 4 bytes)</p></li></ul><p><code>utf8mb4</code> (actual UTF-8)</p><ul><li><p>Max 4 bytes per character</p></li><li><p>Can store emoji</p></li><li><p><strong>This is what you want</strong></p></li></ul><p><strong>The trap:</strong></p><pre><code><code>-- Your table uses default charset (latin1)
CREATE TABLE users (name VARCHAR(100));

-- User tries to save emoji
INSERT INTO users (name) VALUES ('Alice &#128512;');

-- MySQL:
-- &#10060; Error: Incorrect string value
-- OR
-- &#9888;&#65039; Silently truncates emoji
</code></code></pre><p><strong>The fix:</strong></p><pre><code><code>-- Specify utf8mb4 explicitly
CREATE TABLE users (
  name VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
);

-- Or set database default
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
</code></code></pre><h3><strong>PostgreSQL (Mostly Gets It Right)</strong></h3><p>PostgreSQL uses UTF-8 by default. Emoji just work. But <code>CHAR(n)</code> is still usually the wrong type for user input:</p><pre><code><code>CREATE TABLE users (name CHAR(10));
INSERT INTO users (name) VALUES ('Hello &#128075;');

-- This fits, but CHAR pads values with spaces.
</code></code></pre><p><strong>The fix:</strong> Use <code>VARCHAR(n)</code> when you need a character limit, or <code>TEXT</code> when you do not. Avoid <code>CHAR(n)</code> for variable-length user input.</p><h2><strong>Chapter 6: The Application Layer Traps</strong></h2><p>We talked a lot about database level storage till now. However at some point you also have to show the stored text to the users, or atlest process it somehow in your application code. There are many traps there as well.</p><h3><strong>Trap 1: String Length Validation</strong></h3><pre><code><code>// User enters: "Hello &#128104;&#8205;&#128105;&#8205;&#128103;&#8205;&#128102;"
const input = "Hello &#128104;&#8205;&#128105;&#8205;&#128103;&#8205;&#128102;";

// Wrong
if (input.length &gt; 20) {
  throw new Error('Too long');
}
// input.length = 17 (11 for emoji + 6 for "Hello ")

// Right
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const graphemes = Array.from(segmenter.segment(input));
if (graphemes.length &gt; 20) {
  throw new Error('Too long');
}
// graphemes.length = 7 (1 for emoji + 6 for "Hello ")
</code></code></pre><blockquote><p>The choice of using graphemes or actual char count depends on how you want to impose limits on user input.</p></blockquote><h3><strong>Trap 2: Substring Operations</strong></h3><p>If we are having trouble deterministically finding how long a string is, it falls to reason that splitting it would also be hard.</p><pre><code><code>const text = "Hello &#128104;&#8205;&#128105;&#8205;&#128103;&#8205;&#128102; World";

// Wrong
text.substring(0, 10);  // "Hello &#128104;&#8205; " (cuts emoji in half!)

</code></code></pre><p>The solution usually is to use grapheme-aware libraries or avoid substring with emoji &#128540;</p><h3><strong>Trap 3: Regex Matching</strong></h3><p>Regex match is string processing final boss. If you thought splitting string was hard, matching it against a pattern can be a whole new box of wonders. s</p><pre><code><code>// Match any single character
const regex = /^.$/;

'A'.match(regex);           // &#9989; Match
'&#128169;'.match(regex);          // &#10060; No match (2 UTF-16 units)
'&#128104;&#8205;&#128105;&#8205;&#128103;&#8205;&#128102;'.match(regex);      // &#10060; No match (11 UTF-16 units)

// Better
const regex = /^.$/u;  // Unicode flag
'&#128169;'.match(regex);          // &#9989; Match

// But still won't match combined emoji
'&#128104;&#8205;&#128105;&#8205;&#128103;&#8205;&#128102;'.match(regex);      // &#10060; Still no match
</code></code></pre><h2><strong>Chapter 7: The Fixes</strong></h2><p>Despite all the above, the world continues to communicate in emojis, accented characters, regional languages; So there must be some way all of it continues to work. Here are some tips.</p><h3><strong>Fix 1: Use UTF-8 Everywhere</strong></h3><p><strong>Database</strong>: utf8mb4.<br><strong>API responses</strong>: Content-Type: application/json; charset=UTF-8.<br><strong>HTML</strong>: <code>&lt;meta charset="UTF-8"&gt;</code><br><strong>Files</strong>: Save as UTF-8.<br><strong>Environment</strong>: Set LANG=en_US.UTF-8.</p><h3><strong>Fix 2: Normalize User Input</strong></h3><pre><code><code>function normalizeInput(str) {
  return str
    .normalize('NFC') // Normalize to composed form
    .trim(); // Remove whitespace
}
</code></code></pre><h3><strong>Fix 3: Count Graphemes, Not Code Points</strong></h3><pre><code><code>function countGraphemes(str) {
  const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
  return Array.from(segmenter.segment(str)).length;
}
</code></code></pre><h3><strong>Fix 4: Validate Before Storing</strong></h3><pre><code><code>function validateText(str, maxGraphemes, maxBytes) {
  const graphemes = countGraphemes(str);
  const bytes = new TextEncoder().encode(str).length;
  
  if (graphemes &gt; maxGraphemes) {
    throw new Error(`Too many characters (max ${maxGraphemes})`);
  }
  
  if (bytes &gt; maxBytes) {
    throw new Error('Text too long in bytes');
  }
}
</code></code></pre><h3><strong>Fix 5: Use TEXT Columns, Not VARCHAR</strong></h3><pre><code><code>-- Instead of guessing VARCHAR size
CREATE TABLE posts (
  content VARCHAR(10000)  -- Might not be enough for emoji-heavy text
);

-- Use TEXT when there is no application-level character limit
CREATE TABLE posts (
  content TEXT
);
</code></code></pre><p>In PostgreSQL, <code>TEXT</code> and <code>VARCHAR</code> have the same storage characteristics; <code>VARCHAR(n)</code> adds a character limit.</p><h3><strong>Fix 6: Test With Emoji</strong></h3><pre><code><code>// Add these to your test suite
const testStrings = [
  'Simple ASCII',
  'Caf&#233;',                    // Accented characters
  'Hello &#19990;&#30028;',              // CJK characters
  '&#1605;&#1585;&#1581;&#1576;&#1575;',                   // RTL text
  'Hello &#128075;',               // Basic emoji
  '&#128104;&#8205;&#128105;&#8205;&#128103;&#8205;&#128102;',                    // Combined emoji
  '&#127988;&#917607;&#917602;&#917605;&#917614;&#917607;&#917631;',                        // Flag emoji
  '&#128075;&#127995;&#128075;&#127999;',                   // Emoji with skin tones
  '&#8460;&#120098;&#120105;&#120105;&#120108;',                // Mathematical alphanumeric symbols
];
</code></code></pre><h2><strong>Key Takeaways</strong></h2><ol><li><p><strong>Use UTF-8 (utf8mb4 in MySQL) everywhere</strong> - No exceptions</p></li><li><p><strong>Normalize text on input</strong> - NFC is usually the right choice</p></li><li><p><strong>Count graphemes, not code points</strong> - Use Intl.Segmenter</p></li><li><p><strong>Test with emoji</strong> - They will break your assumptions</p></li><li><p><strong>Never use CHAR for user input</strong> - Use VARCHAR or TEXT</p></li><li><p><strong>Validate both length and byte size</strong> - They&#8217;re different</p></li><li><p><strong>Normalize before comparing</strong> - &#8216;&#233;&#8217; has multiple representations</p></li><li><p><strong>Don&#8217;t use names in URLs</strong> - Use IDs instead</p></li></ol><h2><strong>Conclusion</strong></h2><p>Unicode is humanity&#8217;s attempt to represent every written symbol in a computer. Emoji are humanity&#8217;s attempt to communicate without words. Together, they create a perfect storm of edge cases.</p><blockquote><p><strong>Remember:</strong> If your system can&#8217;t handle &#128169;, it&#8217;s not ready for production.</p></blockquote><div class="pullquote"><p>Originally posted on <a href="https://srungta.github.io/">Shaswat Rungta | Personal Blog</a></p></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://shaswatrungta.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hope this was a useful read for you ! Would you like to subscribe to keep in touch? &#128591;&#127995; &#129782;&#127995; &#129392; </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Running Multiple App Copies Locally with Git Worktrees and Caddy]]></title><description><![CDATA[Stable URLs and isolated databases for apps running from several worktrees.]]></description><link>https://shaswatrungta.substack.com/p/running-multiple-app-copies-locally</link><guid isPermaLink="false">https://shaswatrungta.substack.com/p/running-multiple-app-copies-locally</guid><dc:creator><![CDATA[Shaswat Rungta]]></dc:creator><pubDate>Sun, 19 Jul 2026 08:30:34 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!p6QT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!p6QT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!p6QT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png 424w, https://substackcdn.com/image/fetch/$s_!p6QT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png 848w, https://substackcdn.com/image/fetch/$s_!p6QT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png 1272w, https://substackcdn.com/image/fetch/$s_!p6QT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!p6QT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png" width="1200" height="630" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:630,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Three local development slots routed through Caddy like a software pit lane&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Three local development slots routed through Caddy like a software pit lane" title="Three local development slots routed through Caddy like a software pit lane" srcset="https://substackcdn.com/image/fetch/$s_!p6QT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png 424w, https://substackcdn.com/image/fetch/$s_!p6QT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png 848w, https://substackcdn.com/image/fetch/$s_!p6QT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png 1272w, https://substackcdn.com/image/fetch/$s_!p6QT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9275beb8-57c7-4bd7-a1c3-152f0a091ab1_1200x630.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p>&#128218; <strong>Companion post:</strong> </p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;9e02c91a-df0f-4304-8ff1-7c5f9d7f721a&quot;,&quot;caption&quot;:&quot;Use Git worktrees to run agents in parallel without sharing working files or duplicating history.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Git Worktrees for Agentic Development&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:18945353,&quot;name&quot;:&quot;Shaswat Rungta&quot;,&quot;bio&quot;:&quot;I am a tech and doodle enthusiast who also works as a software engineer at Microsoft. This blog is about my experiments with different tools and frameworks. &quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dfc06728-3a65-4ede-8178-9e34e7f20ffe_1418x1418.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2026-07-16T21:02:50.259Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!dF28!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://shaswatrungta.substack.com/p/git-worktrees-for-agentic-development&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:207343023,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:10051216,&quot;publication_name&quot;:&quot;Shaswat Rungta&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!hBp9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfc06728-3a65-4ede-8178-9e34e7f20ffe_1418x1418.jpeg&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>Explains why worktrees are useful when people or coding agents work on several branches at once.</p></blockquote><h1>Development in an agentic world</h1><p>Most local development setups assume that only one copy of the application is running. The frontend always uses one port, the API always uses another, and every branch connects to the same local database. That assumption is invisible while you work on one branch at a time.</p><p>Git worktrees change the equation. <mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">They give each branch a separate directory, but they do not isolate anything the application uses at runtime. </mark>Start the app from a second worktree and both copies compete for the same ports. Change one copy to use different ports and its frontend still needs to find the matching API. Even after both copies start, they can write to the same database.</p><p>This is more than inconvenient bookkeeping. You can open the frontend from one branch while it silently calls the API from another, or test a data change against records created by a different worktree. Two developers or coding agents can interfere with each other even though their source files are isolated.</p><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">What we want is one independently addressable application copy per worktree,</mark> with isolated data, without starting another copy of every heavyweight dependency.</p><p>This post builds that model with <strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">development slots</mark></strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> </mark>and <strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Caddy</mark></strong>. A slot is a name for one running copy of the application and its data boundary. The sample derives that name from the current Git branch, so each worktree naturally gets its own slot. <mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Caddy acts as a shared reverse proxy, giving every slot stable URLs</mark> while its frontend and API run on separate high-numbered ports. You can also provide a slot name explicitly.</p><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Each slot gets:</mark></p><ul><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">A stable frontend URL such as  http://feature-search.multi-agent-wt.localhost:8088</mark></p></li><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">A stable API URL such as http://api-feature-search.multi-agent-wt.localhost:8088</mark></p></li><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Its own frontend and API ports behind those URLs.</mark></p></li><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Its own Cosmos DB database, such as </mark><code>multi-agent-wt-feature-search</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">.</mark></p></li></ul><p>All slots share one Caddy proxy and one Cosmos DB emulator.</p><h2><strong>&#129514; Try the complete sample</strong></h2><p><strong>Direct Link : <span data-color="#6fa8dc" style="color: rgb(111, 168, 220);">https://github.com/srungta/samples/tree/main/MultiAgentWorktrees</span></strong></p><p>The working sample is in <a href="https://github.com/srungta/samples/tree/main/MultiAgentWorktrees">MultiAgentWorktrees</a> Github repo. It contains a React frontend, a .NET API, Docker Compose infrastructure, and the slot script used in this post.</p><p>You need:</p><ul><li><p>Docker Desktop or Docker Engine with Compose</p></li><li><p>.NET 10 SDK</p></li><li><p>Node.js and pnpm</p></li><li><p>Bash and <code>curl</code> (macOS, Linux, or WSL for multi-slot mode)</p></li></ul><p>From the sample directory, run:</p><pre><code><code>./start-multislot.sh infra-up
./start-multislot.sh up</code></code></pre><p>The first command starts the shared Cosmos DB emulator and Caddy. The second command:</p><ol><li><p>Gets the slot name from your current git branch.</p></li><li><p>Assigns stable frontend and API ports.</p></li><li><p>Chooses a database name for the slot; the API creates it on startup.</p></li><li><p>Starts the API and frontend.</p></li><li><p>Adds both hostnames to Caddy.</p></li></ol><p>On the <code>main</code> branch, it prints:</p><pre><code><code>Slot 'main' is ready.
  Frontend: http://main.multi-agent-wt.localhost:8088
  API:      http://api-main.multi-agent-wt.localhost:8088
  Database: multi-agent-wt-main
  Logs:     /Users/you/.config/multi-agent-wt/slots/main</code></code></pre><p>Open the frontend URL and add a note. That note is stored only in this slot&#8217;s database.</p><p>If you are not in a git branch, or want a specific name, pass one:</p><pre><code><code>./start-multislot.sh up demo</code></code></pre><p>That is the entire daily startup flow.</p><h2><strong>&#127807; Run a second branch</strong></h2><p>Create a worktree from your main clone:</p><pre><code><code>git worktree add ../samples-feature-search -b feature/search</code></code></pre><p>Enter that worktree and start its slot:</p><pre><code><code>cd ../samples-feature-search/MultiAgentWorktrees
./start-multislot.sh up</code></code></pre><p>Now both copies run at the same time:</p><p>BranchFrontendDatabase<br><code>main<br>- Frontend: http://main.multi-agent-wt.localhost:8088<br>- Database: multi-agent-wt-main</code></p><p><code>feature/search<br>- Frontend: http://feature-search.multi-agent-wt.localhost:8088<br>- Database: multi-agent-wt-feature-search</code></p><p>Adding a note in one frontend does not make it appear in the other.</p><h2><strong>&#129517; How the pieces fit together</strong></h2><p>At a high level, Caddy sends each hostname to the matching slot. Each slot has its own frontend, API, and database name, while the proxy and database server are shared.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Zo4n!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Zo4n!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png 424w, https://substackcdn.com/image/fetch/$s_!Zo4n!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png 848w, https://substackcdn.com/image/fetch/$s_!Zo4n!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png 1272w, https://substackcdn.com/image/fetch/$s_!Zo4n!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Zo4n!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png" width="757" height="574" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:574,&quot;width&quot;:757,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:49261,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/207635788?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Zo4n!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png 424w, https://substackcdn.com/image/fetch/$s_!Zo4n!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png 848w, https://substackcdn.com/image/fetch/$s_!Zo4n!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png 1272w, https://substackcdn.com/image/fetch/$s_!Zo4n!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dbbd6e0-0d62-4565-a6dc-017763477282_757x574.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Here are the request routes in more detail:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NJGl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NJGl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png 424w, https://substackcdn.com/image/fetch/$s_!NJGl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png 848w, https://substackcdn.com/image/fetch/$s_!NJGl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png 1272w, https://substackcdn.com/image/fetch/$s_!NJGl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NJGl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png" width="1138" height="755" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:755,&quot;width&quot;:1138,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:96182,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/207635788?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NJGl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png 424w, https://substackcdn.com/image/fetch/$s_!NJGl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png 848w, https://substackcdn.com/image/fetch/$s_!NJGl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png 1272w, https://substackcdn.com/image/fetch/$s_!NJGl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8863388b-b429-438b-a2b9-31230e67ead4_1138x755.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>There are two kinds of resources:</p><p><strong>&#127959;&#65039; Shared infrastructure, started once:</strong></p><ul><li><p>Caddy listens on port <code>8088</code> and routes requests by hostname.</p></li><li><p>The Cosmos DB emulator listens on port <code>8081</code> and hosts many databases.</p></li></ul><p><strong>&#128230; Per-slot processes, started in each worktree:</strong></p><ul><li><p>One Vite dev server.</p></li><li><p>One .NET API process.</p></li><li><p>One Cosmos database name.</p></li></ul><h2><strong>&#128256; What Caddy and a reverse proxy do</strong></h2><p>A proxy receives a request on behalf of another server. A <strong>reverse proxy</strong> sits in front of application servers: the browser connects to the proxy, and the proxy chooses which application process should receive the request. The application port stays hidden behind the proxy&#8217;s stable address.</p><p>In this sample, Caddy is the reverse proxy. Every frontend and API URL reaches Caddy on port <code>8088</code>. Caddy reads the HTTP <code>Host</code> header, matches it against a route, and forwards the request to the corresponding process running on a high-numbered host port. The response returns through Caddy to the browser.</p><p>Caddy only handles routing. The worktree isolates source files, while the slot script starts the processes and chooses a database name.</p><h2><strong>&#127760; Why the local hostnames work</strong></h2><p>Names ending in <code>.localhost</code> resolve to the loopback address in modern browsers. You do not need to add every slot to <code>/etc/hosts</code>.</p><p>Caddy receives all traffic on port <code>8088</code>. It uses the request hostname to choose an upstream:</p><pre><code><code>http://feature-search.multi-agent-wt.localhost:8088 {
&#9;reverse_proxy host.docker.internal:40123
}

http://api-feature-search.multi-agent-wt.localhost:8088 {
&#9;reverse_proxy host.docker.internal:20123
}</code></code></pre><p>The actual port numbers do not matter to the person using the app. Caddy hides them behind readable, predictable URLs.</p><p>The Caddy container reaches host-run processes through <code>host.docker.internal</code>. The Compose file adds a host-gateway mapping for Docker Engine on Linux; Docker Desktop provides the same hostname on macOS and Windows.</p><p>The browser calls the frontend hostname to load the UI. The frontend then calls its own API hostname, which also goes through Caddy. Caddy does not forward traffic to Cosmos DB: each API connects directly to the shared emulator.</p><h2><strong>&#128274; Why the data stays isolated</strong></h2><p>Running one Cosmos emulator per worktree would use unnecessary memory. Instead, every API connects to the same endpoint but uses a different database name:</p><pre><code><code>Cosmos__Endpoint="https://localhost:8081"
Cosmos__Database="multi-agent-wt-feature-search"</code></code></pre><p>.NET maps the double underscore in <code>Cosmos__Database</code> to the configuration key <code>Cosmos:Database</code>.</p><p>The sample API uses the current Linux-based Cosmos DB emulator image in HTTPS mode:</p><pre><code><code>cosmos:
  image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-latest
  command: ["--protocol", "https"]
  ports:
    - "8080:8080" # readiness endpoint
    - "8081:8081" # Cosmos endpoint
    - "1234:1234" # Data Explorer</code></code></pre><p>The vNext emulator supports the API for NoSQL in gateway mode, so the .NET client sets it explicitly:</p><pre><code><code>var cosmos = new CosmosClient(endpoint, key, new CosmosClientOptions
{
    ConnectionMode = ConnectionMode.Gateway,
    ServerCertificateCustomValidationCallback = (_, _, _) =&gt; true,
});
</code></code></pre><p>The certificate bypass is for the local emulator only. Do not use it with a production Cosmos DB account.</p><h2><strong>&#128450;&#65039; What the slot script records</strong></h2><p>Each slot has a small state directory under <code>~/.config/multi-agent-wt/slots</code>:</p><pre><code><code>~/.config/multi-agent-wt/slots/feature-search/
&#9500;&#9472;&#9472; env
&#9500;&#9472;&#9472; Caddyfile
&#9500;&#9472;&#9472; api.pid
&#9500;&#9472;&#9472; api.log
&#9500;&#9472;&#9472; frontend.pid
&#9492;&#9472;&#9472; frontend.log</code></code></pre><p>The script lowercases the slot name and replaces non-alphanumeric runs with hyphens. It then hashes that sanitized name into a stable numeric offset, adds the offset to base ports <code>20000</code> and <code>40000</code>, and checks known slots and listening ports for a collision before starting anything.</p><p>The script injects the matching API URL into the Vite process:</p><pre><code><code>VITE_API_BASE=http://api-feature-search.multi-agent-wt.localhost:8088</code></code></pre><p>Vite listens on all host interfaces so Caddy&#8217;s container can reach it:</p><pre><code><code>pnpm exec vite --host 0.0.0.0 --port "$frontend_port" --strictPort</code></code></pre><p>You do not need to copy the full orchestration script from this article. Use the tested <code>start-multislot.sh</code> from the sample.</p><h2><strong>&#128640; What happens when a slot starts</strong></h2><p>Starting a slot does five things:</p><ol><li><p>Pick a name and two free ports for the slot.</p></li><li><p>Start the API and create the slot&#8217;s database if needed.</p></li><li><p>Start the frontend and point it to the slot&#8217;s API.</p></li><li><p>Tell Caddy which URLs belong to the new slot.</p></li><li><p>Check that the frontend and API work. If they do not, stop the slot and remove its Caddy routes.</p></li></ol><p>Caddy and the Cosmos emulator are already running. A new slot adds only a frontend, an API, a database name, and two Caddy routes.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!z5ZV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!z5ZV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png 424w, https://substackcdn.com/image/fetch/$s_!z5ZV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png 848w, https://substackcdn.com/image/fetch/$s_!z5ZV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png 1272w, https://substackcdn.com/image/fetch/$s_!z5ZV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!z5ZV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png" width="1323" height="743" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6483a4fa-1609-4255-a895-809329a6770e_1323x743.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:743,&quot;width&quot;:1323,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:77583,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/207635788?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!z5ZV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png 424w, https://substackcdn.com/image/fetch/$s_!z5ZV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png 848w, https://substackcdn.com/image/fetch/$s_!z5ZV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png 1272w, https://substackcdn.com/image/fetch/$s_!z5ZV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6483a4fa-1609-4255-a895-809329a6770e_1323x743.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>&#128721; Stop and inspect slots</strong></h2><p>List all known slots:</p><pre><code><code>./start-multislot.sh status</code></code></pre><p>Stop only the current branch&#8217;s slot:</p><pre><code><code>./start-multislot.sh down</code></code></pre><p>Stop a named slot:</p><pre><code><code>./start-multislot.sh down feature-search</code></code></pre><p>When no slots remain, stop shared infrastructure:</p><pre><code><code>./start-multislot.sh infra-down</code></code></pre><p>The script refuses to stop shared infrastructure while known slots still exist. Normal shutdown also preserves the Cosmos Docker volume, so your local data survives a restart.</p><h2><strong>&#9851;&#65039; The pattern to reuse</strong></h2><p>You can adapt the sample to another project by changing four things:</p><ol><li><p>Replace <code>multi-agent-wt.localhost</code> with your local domain.</p></li><li><p>Replace the commands that start the frontend and API.</p></li><li><p>Pass a slot-specific database, schema, or tenant name to your backend.</p></li><li><p>Keep shared services in the infrastructure Compose file and lightweight app processes per slot.</p></li></ol><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">The important idea is not the exact script. It is the separation of concerns:</mark></p><ul><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Git worktrees isolate files and branches.</mark></p></li><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Caddy gives changing processes stable names.</mark></p></li><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Per-slot database names isolate data.</mark></p></li><li><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Shared infrastructure keeps the setup light enough to run many copies.</mark></p></li></ul><p>With those boundaries in place, running another branch becomes one command instead of another round of port and configuration bookkeeping.</p><h2><strong>&#9989; Prerequisites for adapting this pattern</strong></h2><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">The orchestration script cannot create isolation if the application has fixed addresses or hidden shared state. </mark>Before applying this pattern to another project, make sure it supports the following capabilities.</p><h3><strong>&#9881;&#65039; Runtime configuration</strong></h3><p>The frontend must read its API base URL from configuration rather than embedding it in the source. The backend must do the same for its database endpoint, credentials, and database name. In this sample, the relevant values are:</p><pre><code><code>VITE_API_BASE=http://api-feature-search.multi-agent-wt.localhost:8088
Cosmos__Endpoint=https://localhost:8081
Cosmos__Database=multi-agent-wt-feature-search</code></code></pre><p>The frontend and API must also accept a port at startup. Each slot needs different listening ports, even though Caddy hides those ports from the browser.</p><h3><strong>&#129521; A per-slot data boundary</strong></h3><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">The shared data service must provide a namespace that can be selected through configuration. </mark>That boundary is a Cosmos database in this sample, but it could be a PostgreSQL database or schema, a storage prefix, or a tenant identifier. Every read, write, migration, and background job must use the selected namespace; one hard-coded database or globally shared queue can break slot isolation.</p><h3><strong>&#128452;&#65039; Repeatable database creation and migration</strong></h3><p>A new slot starts with a new database name, so the application needs an initialization path. The sample API uses <code>CreateDatabaseIfNotExistsAsync</code> and <code>CreateContainerIfNotExistsAsync</code> during startup. <mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">A larger application might run a separate provisioning or migration command instead.</mark></p><p>Whichever approach you use, it should:</p><ul><li><p>Accept the slot-specific database or schema name as input.</p></li><li><p>Be idempotent, so restarting a slot is safe.</p></li><li><p>Apply all schema migrations required by the branch being started.</p></li><li><p>Fail before the slot is advertised as ready if provisioning does not succeed.</p></li></ul><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">Be deliberate about incompatible migrations. </mark>Slots isolate database names, but they still share the same database server or emulator version.</p><h3><strong>&#128268; Reachable services and allowed origins</strong></h3><p>Caddy must be able to reach the frontend and API processes. In the sample they bind to <code>0.0.0.0</code> because Caddy runs in Docker and connects through <code>host.docker.internal</code>. <mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">If Caddy runs directly on the host, binding to loopback may be enough.</mark></p><p>The API must also allow requests from each slot&#8217;s frontend origin. The sample allows every CORS origin for local development. <mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">A real project should generate an allowlist or safely validate the local slot domain instead of carrying that permissive policy into production.</mark></p><h3><strong>&#129658; Readiness and lifecycle commands</strong></h3><p><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">The slot manager needs a lightweight API health endpoint and a frontend URL it can probe before it reports success. </mark>It also needs reliable commands for starting and stopping both processes, plus a place to record their PIDs, logs, assigned ports, and proxy routes.</p><p>Finally, <mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">decide what </mark><code>down</code><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> means for data</mark>. This sample stops the processes and removes the Caddy route but preserves the slot&#8217;s Cosmos database. Projects that need disposable slots should add an explicit database cleanup command rather than deleting data as a side effect of ordinary shutdown.</p><p></p><h1>&#127937; Conclusion </h1><blockquote><p>Remember the last time you switched branches mid-feature, forgot to change a port, and spent twenty minutes wondering why your &#8220;new&#8221; UI was showing old data? That was not a skill issue. That was an invisible assumption biting you. Fix it for yourslef and your agents today &#128526;</p></blockquote><p></p><p></p><div class="pullquote"><p>Originally posted on <a href="https://srungta.github.io/">Shaswat Rungta | Personal Blog</a></p></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://shaswatrungta.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hope this was a useful read for you ! Would you like to subscribe to keep in touch? &#128591;&#127995; &#129782;&#127995; &#129392; </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Git Worktrees for Agentic Development]]></title><description><![CDATA[Give every coding agent an isolated workspace without cloning the repository.]]></description><link>https://shaswatrungta.substack.com/p/git-worktrees-for-agentic-development</link><guid isPermaLink="false">https://shaswatrungta.substack.com/p/git-worktrees-for-agentic-development</guid><dc:creator><![CDATA[Shaswat Rungta]]></dc:creator><pubDate>Thu, 16 Jul 2026 21:02:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!dF28!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><span>Use Git worktrees to run agents in parallel without sharing working files or duplicating history.</span></p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dF28!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dF28!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png 424w, https://substackcdn.com/image/fetch/$s_!dF28!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png 848w, https://substackcdn.com/image/fetch/$s_!dF28!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png 1272w, https://substackcdn.com/image/fetch/$s_!dF28!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dF28!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png" width="1200" height="630" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:630,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Git branching into three isolated agent worktrees&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Git branching into three isolated agent worktrees" title="Git branching into three isolated agent worktrees" srcset="https://substackcdn.com/image/fetch/$s_!dF28!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png 424w, https://substackcdn.com/image/fetch/$s_!dF28!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png 848w, https://substackcdn.com/image/fetch/$s_!dF28!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png 1272w, https://substackcdn.com/image/fetch/$s_!dF28!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F415615bd-d342-402e-8599-bd06ee2553a5_1200x630.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>The problem agents create</strong></h2><p>Most development workflows assume that one developer is doing one thing at a time: check out a branch, make changes, run tests, commit, and move on. In that model, one working directory is plenty.</p><p>AI coding agents break that assumption. The whole point of an agent is that you can run <strong>many at once</strong>: one fixing a bug, one writing tests, one refactoring a module, one exploring a risky idea you&#8217;re not sure about. Suddenly, the working directory becomes a shared resource, and the obvious workarounds start to break down:</p><ul><li><p><strong>Sharing one checkout</strong> &#8212; agents overwrite each other&#8217;s uncommitted files. Agent A&#8217;s half-finished refactor corrupts Agent B&#8217;s test run. <code>git checkout</code> by one agent yanks the ground out from under another.</p></li><li><p><strong>Full </strong><code>git clone</code><strong> per agent</strong> &#8212; every clone re-downloads the entire history. For a large repo that can mean gigabytes and minutes <em>per agent</em>, plus multiple copies of <code>.git</code> consuming disk.</p></li><li><p><strong>Stash juggling</strong> &#8212; <code>git stash</code> to swap contexts is fragile, serial, and impossible to parallelize.</p></li></ul><p>What agents actually need is <strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">isolation without duplication</mark></strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">: </mark>many independent working directories that don&#8217;t interfere, but that share one history so branches, commits, and fetches are instant and cheap.</p><p>Branches alone do not solve this. A branch identifies an independent line of history, but creating one does not create another working directory. For that, each agent needs a worktree of its own.</p><p>That primitive already ships with Git. It is called a <strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">worktree</mark></strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">.</mark></p><div><hr></div><h2><strong>What a worktree actually is</strong></h2><blockquote><p><code>git worktree</code> lets one repository have <strong>multiple working directories checked out at the same time</strong>, each on its own branch, all backed by a <strong>single shared </strong><code>.git</code><strong> object store</strong>.</p></blockquote><pre><code><code># In your main clone (on `main`):
git worktree add -b feature/search ../repo-agent-a main
git worktree add -b bugfix/crash ../repo-agent-b main
git worktree add -b chore/deps ../repo-agent-c main</code></code></pre><p>You now have four directories on disk:</p><pre><code><code>~/code/repo              (main)              &#8592; the primary worktree
~/code/repo-agent-a      (feature/search)
~/code/repo-agent-b      (bugfix/crash)
~/code/repo-agent-c      (chore/deps)</code></code></pre><p>Each directory has its own working files, index, and <code>HEAD</code>. But there is only <strong>one</strong> copy of the repository&#8217;s history: the objects, refs, and packfiles are shared through the repository&#8217;s common Git directory. Creating a worktree does <strong>not</strong> re-download or duplicate that history, so it is typically fast even for a large repository.</p><p>The result is the useful combination: shared history, independent files. That is exactly the isolation parallel agents need.</p><div><hr></div><h2><strong>Why worktrees fit agentic development so well</strong></h2><h3><strong>1. True parallelism with no cross-talk</strong></h3><p>Each agent operates in its own directory. Agent A can run a failing build, rewrite ten files, and run tests &#8212; while Agent B does something completely different &#8212; and neither sees the other&#8217;s uncommitted work. No lockstep, no &#8220;please wait, someone else is building.&#8221;</p><h3><strong>2. Cheap to create and destroy</strong></h3><p>Because history is shared, spinning up a worktree is near-instant and costs only the checked-out files, not another copy of <code>.git</code>. That makes worktrees disposable: create one per task, throw it away when the task merges. Perfect for an orchestrator that launches and reaps agents on demand.</p><pre><code><code>git worktree add ../repo-task-1234 -b agent/task-1234   # create + new branch in one step
# ... agent works, commits, opens a PR ...
git worktree remove ../repo-task-1234                    # reap when done
</code></code></pre><h3><strong>3. One branch per agent, enforced by construction</strong></h3><p>Git <strong>refuses to check out the same branch in two worktrees at once</strong>. That guardrail maps perfectly onto &#8220;one agent owns one branch&#8221;: you literally cannot have two agents accidentally sharing a branch&#8217;s working state.</p><h3><strong>4. Instant context switching for the orchestrator</strong></h3><p>A supervising process (or a human reviewing agent output) can <code>cd</code> between worktrees to inspect each agent&#8217;s in-progress state, run its tests, or diff its changes &#8212; without disturbing any agent. No stashing, no branch swapping, no rebuild-from-scratch.</p><h3><strong>5. Shared fetches and reusable caches</strong></h3><p><code>git fetch</code> in any worktree updates the shared object store, so every worktree sees new upstream commits without separate downloads. Dependency directories remain independent, but package-manager caches can also be shared across worktrees. Together, those two choices keep new agent workspaces cheap to start.</p><div><hr></div><h2><strong>Sharp edges (and how to handle them)</strong></h2><p>Worktrees are powerful but a few things surprise newcomers:</p><ul><li><p><strong>A branch can only be checked out in one worktree.</strong> This is a feature, not a bug &#8212; it prevents two agents from corrupting the same branch. If you need two views of one branch, create a second branch from it (<code>git worktree add ../view -b copy-of-x x</code>) or use a detached checkout (<code>git worktree add --detach ../view x</code>).</p></li><li><p><strong>Untracked files are NOT shared.</strong> <code>node_modules</code>, build outputs, <code>.env</code> files, and other gitignored artifacts exist independently in each worktree. Each agent must install dependencies and generate build artifacts in its own directory &#8212; or you point them at a shared cache (package-manager stores, NuGet, Go, or pip caches, for example) to avoid downloading everything again.</p></li><li><p><strong>Reap orphaned worktrees.</strong> If a directory is deleted without <code>git worktree remove</code>, Git keeps a dangling administrative record. Run <code>git worktree prune</code> (or <code>git worktree remove --force</code>) periodically. <code>git worktree list</code> shows everything currently registered.</p></li><li><p><strong>Submodules need care.</strong> Repos with submodules require <code>git worktree add</code> plus submodule init inside each worktree; plan for it if you use them.</p></li><li><p><strong>Long-lived worktrees drift.</strong> Fetch and rebase/merge in each worktree just like any branch; the shared object store means the fetch is cheap, but the merge is still per-worktree work.</p></li></ul><pre><code><code>git worktree list           # see all worktrees and their branches
git worktree prune          # clean up records for manually-deleted worktrees
git worktree remove &lt;path&gt;  # the correct way to delete a worktree</code></code></pre><div><hr></div><h2><strong>Wrapping up</strong></h2><p>For agentic development, <code>git worktree</code> is the quiet workhorse:</p><ol><li><p><strong>Isolation without duplication</strong> &#8212; many working directories, one shared history.</p></li><li><p><strong>Cheap and disposable</strong> &#8212; spawn a worktree per task, reap it on merge.</p></li><li><p><strong>Safe by construction</strong> &#8212; one branch per worktree keeps agents from corrupting each other.</p></li><li><p><strong>Orchestrator-friendly</strong> &#8212; inspect, test, and diff any agent&#8217;s state without disturbing it.</p></li></ol><p>The important shift is simple: stop treating the repository checkout as shared agent infrastructure. Give every agent its own worktree, and parallel development becomes a set of independent, reproducible workspaces.</p><div class="pullquote"><p>Originally posted on <a href="https://srungta.github.io/">Shaswat Rungta | Personal Blog</a></p></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://shaswatrungta.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hope this was a useful read for you ! Would you like to subscribe to keep in touch? &#128591;&#127995; &#129782;&#127995; &#129392; </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[My Cache Has Trust Issues - A Therapist’s Guide to Cache Invalidation]]></title><description><![CDATA[Is this data fresh? Can I trust it? What if it changed?]]></description><link>https://shaswatrungta.substack.com/p/cache-trust-issues</link><guid isPermaLink="false">https://shaswatrungta.substack.com/p/cache-trust-issues</guid><dc:creator><![CDATA[Shaswat Rungta]]></dc:creator><pubDate>Fri, 06 Mar 2026 18:30:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!SRc1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SRc1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SRc1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png 424w, https://substackcdn.com/image/fetch/$s_!SRc1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png 848w, https://substackcdn.com/image/fetch/$s_!SRc1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png 1272w, https://substackcdn.com/image/fetch/$s_!SRc1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SRc1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png" width="1456" height="1085" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1085,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2184426,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/207244487?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!SRc1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png 424w, https://substackcdn.com/image/fetch/$s_!SRc1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png 848w, https://substackcdn.com/image/fetch/$s_!SRc1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png 1272w, https://substackcdn.com/image/fetch/$s_!SRc1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8688960-1cc8-4dbb-861f-dd7620ed6d2b_1630x1215.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h1>Meeting Your Cache</h1><blockquote><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Therapist:</span></strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);"> </span>Tell me about your cache.</p><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Developer:</span></strong> Well, it&#8217;s&#8230; complicated. Sometimes it works great. Sometimes it serves stale data and users get mad. Sometimes it invalidates too aggressively and my database dies.</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Therapist:</span></strong> It sounds like your cache has trust issues.</p><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Developer:</span></strong> &#8230;is that a thing?</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Therapist:</span></strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);"> </span>Oh yes. Very common. Let&#8217;s start from the beginning.</p></blockquote><h1>The Problem</h1><h2>Cache Doesn&#8217;t Know What&#8217;s True Anymore</h2><p>Your cache has a simple job: <br> <strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> Remember things so you don&#8217;t have to ask the database every time. </mark></strong></p><p>But here&#8217;s the existential crisis: <br><strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> How does the cache know if the data it remembers is still true? </mark></strong></p><pre><code>// Cache: "User 123's email is alice@example.com"
// Reality: Alice just changed her email to alice@newdomain.com
// Cache: "But I remember it being alice@example.com?"
// Reality: "That was 5 minutes ago. Things change."
// Cache: &#128561;&#128561;&#128561; **existential panic**</code></pre><p>This is cache invalidation. It&#8217;s one of the two hard problems in computer science, along with naming things and off-by-one errors.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!u5Zf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!u5Zf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png 424w, https://substackcdn.com/image/fetch/$s_!u5Zf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png 848w, https://substackcdn.com/image/fetch/$s_!u5Zf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png 1272w, https://substackcdn.com/image/fetch/$s_!u5Zf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!u5Zf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png" width="651" height="174" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:174,&quot;width&quot;:651,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:15004,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/207244487?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!u5Zf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png 424w, https://substackcdn.com/image/fetch/$s_!u5Zf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png 848w, https://substackcdn.com/image/fetch/$s_!u5Zf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png 1272w, https://substackcdn.com/image/fetch/$s_!u5Zf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cbefc1e-1c69-4d17-ba69-70d4ac826b3f_651x174.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div><hr></div><h1>Cache Personality Types</h1><h3>Type 1: The Optimist (Time-To-Live Cache)</h3><pre><code>cache.set('user:123', userData, { ttl: 300 }); // 5 minutes</code></pre><p><strong>Personality<br></strong><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">&#8220;I&#8217;ll trust this data for 5 minutes. What could go wrong?&#8221;</mark></p><p><strong>Strengths:</strong></p><ul><li><p>Simple</p></li><li><p>Predictable</p></li><li><p>Doesn&#8217;t overthink things</p></li></ul><p><strong>Weaknesses:</strong></p><ul><li><p>Serves stale data for up to 5 minutes</p></li><li><p>Doesn&#8217;t know when data actually changed</p></li><li><p>Lives in blissful ignorance</p></li></ul><p><strong>When it breaks down:</strong></p><pre><code><strong>// User changes their email</strong>
updateEmail(userId, newEmail);

<strong>// Cache doesn't know for 5 minutes</strong>
cache.get('user:123'); // Still returns old email</code></pre><blockquote><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist:</span></strong> How do you feel about serving stale data?</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Developer:</span></strong> I don&#8217;t like it, but I can&#8217;t check the database every time. That defeats the purpose of caching.</p><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist:</span></strong> What if the data is critical? Like a password change?</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Developer:</span></strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);"> </span><em>nervous sweating</em> I&#8230; I just hope 5 minutes isn&#8217;t too long?</p><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist:</span></strong> That&#8217;s avoidance behavior.</p></blockquote><h3></h3><div><hr></div><h3>Type 2: The Paranoid (Cache-Aside with Validation)</h3><pre><code>const cached = cache.get('user:123');
if (cached &amp;&amp; cached.version === db.getVersion('user:123')) {
  return cached;
}
// Version changed, data is stale
const fresh = db.get('user:123');
cache.set('user:123', fresh);
return fresh;
</code></pre><p><strong>Personality:</strong> <br><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">&#8220;Trust but verify. Actually, mostly verify.&#8221;</mark></p><p><strong>Strengths:</strong></p><ul><li><p>Never serves truly stale data</p></li><li><p>Knows exactly when data changes</p></li><li><p>Catches problems early</p></li></ul><p><strong>Weaknesses:</strong></p><ul><li><p>Still hits the database to check versions</p></li><li><p>If version checking is expensive, you&#8217;ve defeated the purpose</p></li><li><p>Constant anxiety about being wrong</p></li></ul><blockquote><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist:</span></strong> You check the version every single time?</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Developer:</span></strong> What if it changed?</p><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist:</span></strong> But you&#8217;re hitting the database anyway. Why cache at all?</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Developer:</span></strong> Because checking the version is cheaper than fetching all the data! &#8230;right?</p><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist:</span></strong> Is it though?</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Developer:</span></strong> <em>existential crisis intensifies</em></p></blockquote><h3></h3><div><hr></div><h3>Type 3: The Control Freak (Write-Through Cache)</h3><pre><code>function updateUser(userId, newData) {
  // Update database
  db.update('users', userId, newData);
  
  // Immediately update cache
  cache.set(`user:${userId}`, newData);
}
</code></pre><p><strong>Personality:</strong> <br><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">&#8220;I control everything. If data changes, I&#8217;ll know because </mark><em><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">I&#8217;m</mark></em><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);"> the one changing it.&#8221;</mark></p><p><strong>Strengths:</strong></p><ul><li><p>Cache is always consistent with writes</p></li><li><p>No stale data for writes you control</p></li><li><p>Clear ownership</p></li></ul><p><strong>Weaknesses:</strong></p><ul><li><p>What about writes from other servers?</p></li><li><p>What about manual database updates?</p></li><li><p>What about data that changes from external systems?</p></li></ul><p><strong> </strong></p><blockquote><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist</span>:</strong> What happens if someone updates the database directly?</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Cache</span>:</strong> That&#8217;s not allowed. All writes go through me.</p><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist</span>:</strong> But what if they do anyway?</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Cache</span>:</strong> THEY CAN&#8217;T. I CONTROL THE DATA.</p><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist</span>:</strong> This is concerning. You&#8217;re exhibiting control issues.</p></blockquote><h3></h3><div><hr></div><h3>Type 4: The Anxious Overthinker (Event-Driven Invalidation)</h3><pre><code>// When data changes, publish an event
eventBus.publish('user.updated', { userId: 123 });

// Cache listens for events
eventBus.subscribe('user.updated', (event) =&gt; {
  cache.delete(`user:${event.userId}`);
});
</code></pre><p><strong>Personality:</strong> <br><mark data-color="#fff2cc" style="background-color: rgb(255, 242, 204); color: rgb(0, 0, 0);">&#8220;I need to know the moment ANYTHING changes ANYWHERE.&#8221;</mark></p><p><strong>Strengths:</strong></p><ul><li><p>Invalidates immediately when data changes</p></li><li><p>No TTL guessing</p></li><li><p>Proactive, not reactive</p></li></ul><p><strong>Weaknesses:</strong></p><ul><li><p>What if the event gets lost?</p></li><li><p>What if events arrive out of order?</p></li><li><p>What if the event system is down?</p></li><li><p>Constant monitoring of event streams</p></li></ul><p><strong> </strong></p><blockquote><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist</span>:</strong> You&#8217;re subscribed to 47 different event topics?</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Cache</span>:</strong> I need to know when things change!</p><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist</span>:</strong> But you&#8217;re spending all your time processing events instead of actually caching.</p><p><strong><span data-color="#0b5394" style="color: rgb(11, 83, 148);">Cache</span>:</strong> What if I miss an update?</p><p><strong><span data-color="#a64d79" style="color: rgb(166, 77, 121);">Therapist</span>:</strong> This is anxiety. You&#8217;re catastrophizing.</p></blockquote><p></p><h2>Common Cache Anxieties</h2><h3>Anxiety 1: &#8220;What if I invalidate too early?&#8221;</h3><pre><code>cache.set('user:123', data, { ttl: 10 }); // 10 seconds

// 5 seconds later
cache.delete('user:123'); // Oops, data was still fresh

// Now every request hits the database &#128680;</code></pre><p><strong>Symptom:</strong> Over-invalidation. Cache has no confidence in its own data.</p><p><strong>Treatment:</strong> Use longer TTLs with selective invalidation for critical paths.</p><h3>Anxiety 2: &#8220;What if I invalidate too late?&#8221;</h3><pre><code>cache.set('user:123', data, { ttl: 3600 }); // 1 hour

// User changes password
updatePassword(userId, newPassword);

// Cache still serves old password hash for up to 1 hour
// User can't log in with new password
</code></pre><p><strong>Symptom:</strong> Stale data causes user-facing bugs.</p><p><strong>Treatment:</strong> Invalidate explicitly on writes. Don&#8217;t rely solely on TTL for critical data.</p><h3>Anxiety 3: &#8220;What if multiple servers invalidate at different times?&#8221;</h3><pre><code>// Server A
cache.delete('user:123');

// Server B (didn't get the memo)
cache.get('user:123'); // Still has stale data
</code></pre><p><strong>Symptom:</strong> Distributed cache inconsistency. Different servers see different reality.</p><p><strong>Treatment:</strong> Use a centralized cache (Redis) or cache invalidation events.</p><h3>Anxiety 4: &#8220;What if the database and cache disagree?&#8221;</h3><pre><code>// Database says: email = alice@new.com
// Cache says: email = alice@old.com

// Who's right? Cache doesn't know.
</code></pre><p><strong>Symptom:</strong> Split-brain. Truth has diverged.</p><p><strong>Treatment:</strong> Database is always the source of truth. When in doubt, invalidate and refetch.</p><h2>The Cache Invalidation Patterns</h2><h3>Pattern 1: Lazy Invalidation (TTL)</h3><p><strong>How it works:</strong> Set a timer. When timer expires, delete from cache.</p><p><strong>Pros:</strong> Simple. Works everywhere.</p><p><strong>Cons:</strong> Serves stale data until expiration.</p><p><strong>Best for:</strong> Non-critical data that doesn&#8217;t change often (user profiles, settings).</p><p><strong>Cache personality:</strong> Optimistic.</p><h3>Pattern 2: Eager Invalidation (Write-Through)</h3><p><strong>How it works:</strong> Every write updates both database and cache.</p><p><strong>Pros:</strong> Cache is always fresh after writes.</p><p><strong>Cons:</strong> Doesn&#8217;t catch external updates. Adds latency to writes.</p><p><strong>Best for:</strong> Write-heavy workloads where you control all writes.</p><p><strong>Cache personality:</strong> Control freak.</p><h3>Pattern 3: Event-Driven Invalidation</h3><p><strong>How it works:</strong> Publish events when data changes. Cache subscribes and invalidates.</p><p><strong>Pros:</strong> Near-instant invalidation. Works across servers.</p><p><strong>Cons:</strong> Complex. Event system becomes a dependency. Eventual consistency.</p><p><strong>Best for:</strong> Distributed systems with multiple writers.</p><p><strong>Cache personality:</strong> Anxious overthinker.</p><h3>Pattern 4: Versioned Data</h3><p><strong>How it works:</strong> Store version number with data. Check version before using cache.</p><p><strong>Pros:</strong> Catches all changes. Works with any write pattern.</p><p><strong>Cons:</strong> Extra database query to check version.</p><p><strong>Best for:</strong> Critical data where staleness is unacceptable.</p><p><strong>Cache personality:</strong> Paranoid.</p><h3>Pattern 5: No Cache (The Nuclear Option)</h3><p><strong>How it works:</strong> Don&#8217;t cache. Just hit the database every time.</p><p><strong>Pros:</strong> Never stale. Simple. No invalidation needed.</p><p><strong>Cons:</strong> Slower. Database load increases.</p><p><strong>Best for:</strong> Data that changes constantly or is queried rarely.</p><p><strong>Cache personality:</strong> Has given up on therapy.</p><h2>Real-World Scenarios</h2><h3>Scenario 1: User Profile</h3><p><strong>Data:</strong> Name, email, profile picture<br><strong>Change frequency:</strong> Rarely<br><strong>Staleness tolerance:</strong> High (users understand profile changes take a moment)<br><strong>Solution:</strong> TTL cache with 5-minute expiration + eager invalidation on profile updates</p><pre><code>function updateProfile(userId, newData) {
  db.update('users', userId, newData);
  cache.set(`user:${userId}`, newData, { ttl: 300 });
}

function getProfile(userId) {
  const cached = cache.get(`user:${userId}`);
  if (cached) return cached;
  
  const data = db.get('users', userId);
  cache.set(`user:${userId}`, data, { ttl: 300 });
  return data;
}
</code></pre><h3>Scenario 2: Inventory Count</h3><p><strong>Data:</strong> Number of items in stock<br><strong>Change frequency:</strong> Often (every purchase)<br><strong>Staleness tolerance:</strong> Low (can&#8217;t oversell)<br><strong>Solution:</strong> Don&#8217;t cache. Or cache with very short TTL + aggressive invalidation.</p><pre><code>function getInventory(productId) {
  // Don't cache. Always get fresh count.
  return db.get('inventory', productId);
}

// OR cache for 10 seconds max
function getInventory(productId) {
  const cached = cache.get(`inventory:${productId}`);
  if (cached) return cached;
  
  const data = db.get('inventory', productId);
  cache.set(`inventory:${productId}`, data, { ttl: 10 });
  return data;
}

function purchaseItem(productId) {
  db.decrementInventory(productId);
  cache.delete(`inventory:${productId}`); // Invalidate immediately
}
</code></pre><h3>Scenario 3: Session Data</h3><p><strong>Data:</strong> User&#8217;s login session, preferences<br><strong>Change frequency:</strong> Rare (only when user logs in/out)<br><strong>Staleness tolerance:</strong> None (security-critical)<br><strong>Solution:</strong> Cache with no TTL + explicit invalidation on logout</p><pre><code>function createSession(userId, sessionData) {
  const sessionId = generateId();
  cache.set(`session:${sessionId}`, sessionData); // No TTL
  return sessionId;
}

function logout(sessionId) {
  cache.delete(`session:${sessionId}`); // Explicit invalidation
}
</code></pre><h2>The Two Rules of Cache Therapy</h2><h3>Rule 1: Cache is a Hint, Not Truth</h3><p>The database is the source of truth. Cache is a guess about what the database says.</p><p>If cache and database disagree, database wins. Always.</p><h3>Rule 2: Design for Staleness</h3><p>Don&#8217;t build systems that break when cache is stale. Build systems that gracefully handle stale data.</p><p><strong>Bad:</strong> User changes password. Old password works for 5 minutes because cache.</p><p><strong>Good:</strong> User changes password. Old password stops working immediately (password check always hits database). Profile picture might be stale for 5 minutes (that&#8217;s fine).</p><h2>Key Takeaways</h2><ol><li><p><strong>All caches serve stale data eventually</strong> - The question is how long you can tolerate</p></li><li><p><strong>TTL is a guess</strong> - You&#8217;re guessing how long data stays valid</p></li><li><p><strong>Invalidation is hard in distributed systems</strong> - Multiple servers, eventual consistency, event ordering</p></li><li><p><strong>Cache the read path, not the write path</strong> - Writes should invalidate, not update cache</p></li><li><p><strong>Critical data shouldn&#8217;t be cached</strong> - Or cache with very short TTLs and explicit invalidation</p></li><li><p><strong>Monitor your cache hit rate</strong> - If it&#8217;s too low, you&#8217;re over-invalidating</p></li><li><p><strong>Trust issues are normal</strong> - Cache invalidation is legitimately difficult</p></li></ol><h2>Conclusion</h2><p>Your cache has trust issues because the world is untrustworthy. Data changes. Networks fail. Events get lost. Clocks drift.</p><p>The best you can do is:</p><ol><li><p>Accept that cache will occasionally be stale</p></li><li><p>Design systems that tolerate staleness</p></li><li><p>Invalidate aggressively for critical data</p></li><li><p>Monitor and adjust TTLs based on real behavior</p></li></ol><p>And maybe, just maybe, your cache will learn to trust again.</p><p><strong>Remember:</strong> </p><blockquote><p><mark data-color="#fce5cd" style="background-color: rgb(252, 229, 205); color: rgb(0, 0, 0);">The only thing worse than a stale cache is no cache at all. <br>And the only thing worse than no cache is a cache that lies to you.</mark></p></blockquote>]]></content:encoded></item><item><title><![CDATA[The Internet is Held Together by Duct Tape]]></title><description><![CDATA[The tech debt of the Internet]]></description><link>https://shaswatrungta.substack.com/p/internet-duct-tape</link><guid isPermaLink="false">https://shaswatrungta.substack.com/p/internet-duct-tape</guid><dc:creator><![CDATA[Shaswat Rungta]]></dc:creator><pubDate>Tue, 03 Mar 2026 18:30:00 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!b0NJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!b0NJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png 424w, https://substackcdn.com/image/fetch/$s_!b0NJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png 848w, https://substackcdn.com/image/fetch/$s_!b0NJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png 1272w, https://substackcdn.com/image/fetch/$s_!b0NJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!b0NJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:700341,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://shaswatrungta.substack.com/i/207244489?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!b0NJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png 424w, https://substackcdn.com/image/fetch/$s_!b0NJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png 848w, https://substackcdn.com/image/fetch/$s_!b0NJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png 1272w, https://substackcdn.com/image/fetch/$s_!b0NJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8101c410-7bf0-486b-a9a9-4ec6520533fb_1600x2030.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="7360" height="4912" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:4912,&quot;width&quot;:7360,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;yellow banana fusion bead&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="yellow banana fusion bead" title="yellow banana fusion bead" srcset="https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1577783962414-a36fa2188247?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxkdWN0JTIwdGFwZXxlbnwwfHx8fDE3ODQxOTI2NzF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@switch_dtp_fotografie">Lucas van Oort</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><h2>The tech debt of the Internet</h2><p>The internet is a miracle of engineering. It is also held together by hacks, workarounds, and solutions that made everyone say &#8220;this is temporary until we fix it properly.&#8221;</p><p>Some of these we use everyday without noticing. Lets see how they came to be.</p><h2>Hack 1: <a href="https://en.wikipedia.org/wiki/JSONP">JSONP</a> (JavaScript Object Notation with Padding)</h2><h3>The Problem</h3><p>You want to fetch data from a different domain. The browser says no:</p><pre><code>fetch("https://api.example.com/data")
  .then((r) =&gt; r.json())
  .then((data) =&gt; console.log(data));

// &#10060; Error: No 'Access-Control-Allow-Origin' header
</code></pre><p><a href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy">Same-origin policy</a> blocks you. In the mid 2000s, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS</a> doesn&#8217;t exist yet. What do you do?</p><h3>The Terrible Solution</h3><p><strong>Observation:</strong> <code>&lt;script&gt;</code> tags can load JavaScript from anywhere.</p><p><strong>Terrible idea:</strong> What if we put JSON data <em>inside</em> a JavaScript file?</p><pre><code>// Your page
function handleData(data) {
  console.log(data);
}

// Their server returns this:
handleData({ user: "Alice", email: "alice@example.com" });

// You load it with a script tag:
&lt;script src="https://api.example.com/data?callback=handleData"&gt;&lt;/script&gt;;
</code></pre><p><strong>It works!</strong> The server wraps JSON in a function call. Your page defines the function. Script tag loads and executes. You have your data.</p><p><strong>Why it&#8217;s terrible:</strong></p><ul><li><p>The server can execute arbitrary JavaScript in your page</p></li><li><p>No error handling</p></li><li><p>URL length limits</p></li><li><p>Can only do GET requests</p></li><li><p>The word &#8220;padding&#8221; is a euphemism for &#8220;wrapping JSON in a function call like savages&#8221;</p></li></ul><p><strong>Why it lasted so long:</strong><br>Because it worked. And CORS took years to standardize and implement everywhere.</p><p><strong>Status in 2026:</strong> Officially dead. Unofficially, someone somewhere is still using it and their code will break when browsers finally remove support.</p><h2>Hack 2: <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request">CORS Preflight</a> (The OPTIONS Request Nobody Wanted)</h2><h3>The Problem</h3><p>It is 2005 and CORS exists now! You can make cross-origin requests! Except&#8230; sometimes the browser sends TWO requests:</p><pre><code>fetch("https://api.example.com/data", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ data: "hi" }),
});

// Browser sends:
// 1. OPTIONS request (preflight)
// 2. POST request (your actual request)
</code></pre><p><strong>Why two requests?</strong> Because the browser doesn&#8217;t trust you (and rightly so!).</p><h3>The Terrible Solution</h3><p>The OPTIONS request is a &#8220;pre-flight&#8221; check. The browser asks the server: &#8220;Is it okay if I send a POST request with this header?&#8221;</p><pre><code>OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://yoursite.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

// Server responds:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://yoursite.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
</code></pre><p>Only then does the browser send your actual POST request.</p><p><strong>Why it&#8217;s terrible:</strong></p><ul><li><p>Doubles the number of requests</p></li><li><p>Adds latency</p></li><li><p>Server has to handle OPTIONS requests for every endpoint</p></li><li><p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age"><code>Access-Control-Max-Age</code></a> is supposed to cache the preflight, but browsers cap it (Chrome ~2 hours, Firefox ~24 hours)</p></li><li><p>Many developers don&#8217;t know why OPTIONS requests exist and misconfigure their servers</p></li></ul><p><strong>Why it exists:</strong><br>Security. The browser wants to protect the server from dangerous requests. But also backward compatibility - old servers that don&#8217;t understand CORS shouldn&#8217;t accidentally accept dangerous requests.</p><p><strong>Status in 2026:</strong> Still here. Still annoying. Still necessary. Will outlive us all.</p><h2>Hack 3: HTTP Polling (Asking &#8220;Are We There Yet?&#8221; Every Second)</h2><h3>The Problem (2005)</h3><p>You want real-time updates. <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API">WebSockets</a> don&#8217;t exist. How do you get the server to push data to the client?</p><h3>The Terrible Solution</h3><p>Just ask the server for updates every second:</p><pre><code>setInterval(() =&gt; {
  fetch("/updates")
    .then((r) =&gt; r.json())
    .then((data) =&gt; {
      if (data.newMessages) {
        showMessages(data.newMessages);
      }
    });
}, 1000); // Ask every second
</code></pre><p><strong>Why it&#8217;s terrible:</strong></p><ul><li><p>Makes 60 requests per minute even when nothing happens</p></li><li><p>Wastes bandwidth</p></li><li><p>Wastes server resources</p></li><li><p>Wastes battery on mobile devices</p></li><li><p>Still has latency (updates only every second)</p></li></ul><p><strong>Variations:</strong></p><p><strong><a href="https://en.wikipedia.org/wiki/Push_technology#Long_polling">Long polling</a>:</strong> Keep the connection open until there&#8217;s data</p><pre><code>function poll() {
  fetch("/updates")
    .then((r) =&gt; r.json())
    .then((data) =&gt; {
      handleData(data);
      poll(); // Immediately poll again
    });
}
</code></pre><p>Server holds the request open until there&#8217;s data, then responds. Client immediately polls again.</p><p><strong>Why it&#8217;s also terrible:</strong> Keeps connections open forever. But at least it doesn&#8217;t waste requests when nothing happens.</p><p><strong>Status in 2026:</strong> WebSockets exist now. But polling still lives on in:</p><ul><li><p>Systems that can&#8217;t use WebSockets (restrictive firewalls)</p></li><li><p>&#8220;Enterprise&#8221; systems that haven&#8217;t been updated since 2008</p></li><li><p>Systems that consider Server Sent Events and WebSockets too complex a solution for their use case.</p></li></ul><h2>Hack 4: Base64 Images in CSS</h2><h3>The Problem</h3><p><a href="https://datatracker.ietf.org/doc/html/rfc2616">HTTP/1.1</a> has a limit on concurrent connections. Every image is a separate request. Loading 50 icons = 50 requests = slow.</p><h3>The Terrible Solution</h3><p>Encode images as <a href="https://developer.mozilla.org/en-US/docs/Glossary/Base64">base64</a> strings and embed them directly in CSS as <a href="https://developer.mozilla.org/en-US/docs/Web/URI/Schemes/data">data URLs</a>:</p><pre><code>.icon {
  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA
AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL
0Y4OHwAAAABJRU5ErkJggg==);
}
</code></pre><p><strong>Why it&#8217;s terrible:</strong></p><ul><li><p>Base64 is 33% larger than binary</p></li><li><p>Can&#8217;t be cached separately from CSS</p></li><li><p>Makes CSS files huge</p></li><li><p>Can&#8217;t lazy-load images</p></li><li><p>Makes CSS unreadable</p></li></ul><p><strong>Why it was used:</strong></p><ul><li><p>Reduces HTTP requests (important in HTTP/1.1)</p></li><li><p>Entire CSS file can be cached</p></li><li><p>No <a href="https://en.wikipedia.org/wiki/Flash_of_unstyled_content">FOUC</a> (Flash of Unstyled Content) waiting for images</p></li></ul><p><strong>Status in 2026:</strong> <a href="https://datatracker.ietf.org/doc/html/rfc7540">HTTP/2</a> fixed the connection limit problem. But you still see base64 images in:</p><ul><li><p>Old codebases</p></li><li><p>Email HTML (email clients block external images)</p></li><li><p>Single-file HTML apps (everything in one file)</p></li><li><p>Systems where image hosting over CDNs is relatively expensive.</p></li><li><p>The image is small enough, what&#8217;s the harm?(!)</p></li></ul><h2>Hack 5: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent">User-Agent</a> Sniffing</h2><h3>The Problem</h3><p>Different browsers support different features. You need to know which browser the user has.</p><h3>The Terrible Solution</h3><p>Check the <code>User-Agent</code> string:</p><pre><code>const ua = navigator.userAgent;

if (ua.includes("MSIE") || ua.includes("Trident")) {
  // Internet Explorer detected
  loadIEPolyfills();
}

if (ua.includes("Chrome")) {
  // Use Chrome-specific features
}
</code></pre><p><strong>Why it&#8217;s terrible:</strong></p><ul><li><p>User-Agent strings lie</p></li><li><p>Browsers spoof each other to avoid being blocked</p></li><li><p>User-Agent strings are ridiculously long and complicated</p></li><li><p>Feature detection is better than browser detection</p></li></ul><p><strong>Example User-Agent:</strong></p><pre><code>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59
</code></pre><p>This is Edge pretending to be Chrome pretending to be Safari pretending to be Mozilla. Why? Because if a website blocked Edge, this partially circumvents the ban.</p><p><strong>Feature detection</strong></p><p>A much better approach is to use [Feature detection][feature-detection]. Check for presence of the feature in your JS <strong>Example:</strong></p><pre><code>if ("geolocation" in navigator) {
  // Use geolocation
}

if (typeof Promise !== "undefined") {
  // Use promises
}
</code></pre><p><strong>Status in 2026:</strong> User-Agent sniffing is officially discouraged. But it&#8217;s still everywhere because old code never dies.</p><h2>Hack 6: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies">Cookies</a> for Everything</h2><h3>The Problem (1994)</h3><p>HTTP is stateless. You need to remember who the user is between requests.</p><h3>The Terrible Solution</h3><p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies">Cookies</a>! A small piece of data sent with every request:</p><pre><code>Set-Cookie: sessionId=abc123; Path=/; HttpOnly; Secure
</code></pre><p><strong>Why it&#8217;s terrible:</strong></p><ul><li><p>Sent with EVERY request, even for images/CSS/JS</p></li><li><p>Limited to 4 KB per cookie</p></li><li><p>No structured data format</p></li><li><p>Global scope by default (every subdomain can read it)</p></li><li><p>Security nightmare (<a href="https://owasp.org/www-community/attacks/xss/">XSS</a> can steal cookies, <a href="https://owasp.org/www-community/attacks/csrf">CSRF</a> exploits them)</p></li></ul><p><strong>Why it lasted:</strong><br>Because nothing better existed. We needed some way to do sessions.</p><p><strong>What cookies are used for:</strong></p><ul><li><p>Authentication (session tokens)</p></li><li><p>Tracking (ads, analytics)</p></li><li><p>User preferences</p></li><li><p>Shopping carts</p></li><li><p>CSRF tokens (ironically, to protect against CSRF)</p></li></ul><p><strong>Status in 2026:</strong> We have <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"><code>localStorage</code></a>, <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage"><code>sessionStorage</code></a>, <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API"><code>IndexedDB</code></a>. But cookies persist because:</p><ul><li><p>They&#8217;re automatically sent with requests (good for auth)</p></li><li><p>They work across tabs</p></li><li><p>Old code depends on them</p></li></ul><h2>Hack 7: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/write">document.write()</a> (The Original Sin)</h2><h3>The Problem</h3><p>You want to add content to a page dynamically.</p><h3>The Terrible Solution</h3><pre><code>document.write("&lt;div&gt;Hello World&lt;/div&gt;");
</code></pre><p><strong>Why it&#8217;s terrible:</strong></p><ul><li><p>If called after page load, it WIPES THE ENTIRE PAGE</p></li><li><p>Blocks page parsing</p></li><li><p>Can&#8217;t be used in async scripts</p></li><li><p>Messes with the DOM</p></li><li><p>Everyone who has used it has a horror story</p></li></ul><p><strong>Why it existed:</strong><br>In 1995, there was no <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model">DOM API</a>. <code>document.write()</code> was the only way to generate HTML from JavaScript.</p><p><strong>Status in 2026:</strong> Not officially deprecated in the spec, but strongly discouraged. (See the mozilla document page on this and you will realize how red it is with warnings.) Chrome actively blocks it in some scenarios. Still in millions of ad scripts because ad tech is where code goes to die.</p><blockquote><p>It was a reliable way to ensure the ad script executed and rendered its content across a variety of older browser environments without needing more complex DOM manipulation methods. Ad scripts often use document.write() to inject further</p></blockquote><h2>Hack 8: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval">eval()</a> (The Devil&#8217;s Function)</h2><h3>The Problem</h3><p>You need to execute dynamically constructed code. Maybe you&#8217;re parsing a data format (before <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse"><code>JSON.parse()</code></a> existed), maybe you&#8217;re building a template engine, or maybe you&#8217;re just making questionable life choices.</p><h3>The Terrible Solution</h3><pre><code>const code = 'console.log("Hello")';
eval(code); // Executes the string as JavaScript
</code></pre><p><strong>Why it&#8217;s terrible:</strong></p><ul><li><p>Executes arbitrary code</p></li><li><p>Security nightmare</p></li><li><p>Performance nightmare (can&#8217;t be optimized)</p></li><li><p>Debugging nightmare</p></li><li><p>Makes code review impossible</p></li></ul><p><strong>Why it exists:</strong><br>Sometimes you legitimately need to execute dynamic code. JSONP used <code>eval()</code> before <code>JSON.parse()</code> existed.</p><p><strong>Status in 2026:</strong> Still exists. Still dangerous. Still used in:</p><ul><li><p>Code playgrounds</p></li><li><p>Template engines</p></li><li><p>Dynamic query builders</p></li><li><p>Bad decisions</p></li></ul><h2>Hack 9: The <strong>&lt;table&gt;</strong> Layout Era</h2><h3>The Problem (1995)</h3><p>You want to create a layout with columns. CSS layout support barely exists.</p><h3>The Terrible Solution</h3><p>Use HTML tables for everything:</p><pre><code>&lt;table&gt;
  &lt;tr&gt;
    &lt;td&gt;Sidebar&lt;/td&gt;
    &lt;td&gt;Main Content&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
</code></pre><p><strong>Why it&#8217;s terrible:</strong></p><ul><li><p>Semantically wrong (tables are for data, not layout)</p></li><li><p>Accessibility nightmare</p></li><li><p>Inflexible</p></li><li><p>Nested tables everywhere</p></li><li><p>Screen readers think everything is tabular data</p></li></ul><p><strong>Status in 2026:</strong> Finally dead. If you see table layouts in 2026, the website is either:</p><ul><li><p>Very old</p></li><li><p>Generated by an ancient CMS</p></li><li><p>An email (email clients still use table layouts)</p></li></ul><blockquote><p>Please just use flexbox or css grids for your layouting.</p></blockquote><h2>Hack 10: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/important">!important</a> (The CSS Nuclear Option)</h2><h3>The Problem</h3><p>Your CSS rule isn&#8217;t being applied. Something else is overriding it. You don&#8217;t know what, and you don&#8217;t have time to figure it out.</p><h3>The Terrible Solution</h3><pre><code>.button {
  color: red !important;
  background: blue !important;
  margin: 10px !important;
  /* I don't understand the cascade and at this point I'm afraid to ask */
}
</code></pre><p><strong>Why it&#8217;s terrible:</strong></p><ul><li><p>Breaks the natural <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade">cascade</a> that makes CSS work</p></li><li><p>The only way to override <code>!important</code> is with another <code>!important</code></p></li><li><p>Leads to specificity wars that nobody wins</p></li><li><p>Makes CSS unmaintainable &#8212; you can&#8217;t tell what&#8217;s overriding what</p></li><li><p>Commonly a sign that the real problem is poorly structured selectors</p></li></ul><p><strong>Why it&#8217;s used:</strong></p><ul><li><p>Third-party widgets inject styles you can&#8217;t control</p></li><li><p>Legacy CSS is too tangled to refactor safely</p></li><li><p>Deadline pressure: &#8220;just slap <code>!important</code> on it and ship&#8221;</p></li><li><p>It always works (that&#8217;s the dangerous part)</p></li></ul><p><strong>Status in 2026:</strong> Alive and thriving. Every codebase has at least one <code>!important</code>. Most have hundreds. <a href="https://en.wikipedia.org/wiki/CSS-in-JS">CSS-in-JS</a> and utility frameworks like <a href="https://tailwindcss.com/">Tailwind</a> reduce the need, but whenever CSS gets complicated, <code>!important</code> is the first thing developers reach for.</p><h2>Why These Hacks Matter</h2><h3>Lesson 1: Temporary Solutions Become Permanent</h3><p>JSONP was a hack. It was supposed to be replaced by CORS. It lasted 15+ years.</p><h3>Lesson 2: Backward Compatibility is Forever</h3><p>We can&#8217;t remove old features because someone, somewhere, depends on them.</p><h3>Lesson 3: Constraints Breed Creativity</h3><p>These hacks exist because smart people solved real problems with limited tools.</p><h3>Lesson 4: The Perfect Solution Never Comes</h3><p>CORS is &#8220;better&#8221; than JSONP, but it&#8217;s also more complex, has preflight requests, and still frustrates developers daily.</p><h2>Key Takeaways</h2><ol><li><p><strong>Every hack was once a clever solution</strong> - JSONP was genius in 2005</p></li><li><p><strong>Temporary hacks become permanent</strong> - Plan accordingly</p></li><li><p><strong>Backward compatibility preserves terrible ideas</strong> - The web can never fully break old sites</p></li><li><p><strong>Standards take years</strong> - Hacks fill the gap</p></li><li><p><strong>Your clever hack will haunt you</strong> - Future you will curse past you</p></li></ol><h2>Conclusion</h2><p>The internet works. Millions of websites, billions of users, trillions of requests per day. And it&#8217;s all held together by solutions that were supposed to be temporary.</p><p>JSONP. CORS preflight. HTTP polling. Base64 images. Cookies for everything. These are the duct tape and baling wire of the web.</p><p>And you know what? <strong>It works.</strong> Not elegantly. Not beautifully. But it works.</p><p>Next time you&#8217;re tempted to call something a &#8220;hack,&#8221; remember: the entire internet is a hack. Your hack is in good company.</p><div><hr></div>]]></content:encoded></item><item><title><![CDATA[Making Your Codebase LLM Friendly]]></title><description><![CDATA[Simple practices to help AI assistants understand and work with your code effectively]]></description><link>https://shaswatrungta.substack.com/p/llm-friendly-codebases</link><guid isPermaLink="false">https://shaswatrungta.substack.com/p/llm-friendly-codebases</guid><dc:creator><![CDATA[Shaswat Rungta]]></dc:creator><pubDate>Mon, 02 Mar 2026 18:30:00 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="6151" height="3724" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3724,&quot;width&quot;:6151,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;a person holding a robotic hand in front of a mirror&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="a person holding a robotic hand in front of a mirror" title="a person holding a robotic hand in front of a mirror" srcset="https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1682159672286-40790338349b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxOXx8ZnJpZW5kbHklMjBhaXxlbnwwfHx8fDE3ODQxOTI1MzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@katjaano">Katja Ano</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><h2>What Does &#8220;LLM Friendly&#8221; Really Mean?</h2><p>Think about the last time someone new joined your team. What did they struggle with?</p><ul><li><p>Finding where things are</p></li><li><p>Understanding why things work a certain way</p></li><li><p>Figuring out what patterns to follow</p></li><li><p>Knowing what&#8217;s okay to change and what isn&#8217;t</p></li></ul><p>LLMs face the exact same challenges. An &#8220;LLM friendly&#8221; codebase is simply one where these answers are easy to find.</p><blockquote><p>The trick is to write code that anyone (or anything &#128521;) can understand.</p></blockquote><h2>Why This Matters Now</h2><p>LLMs are already writing code in production systems. Tools like GitHub Copilot, Codex, and Claude are generating pull requests, fixing bugs, and adding features. The question isn&#8217;t whether to use them - it&#8217;s how to use them effectively.</p><p>I have been using most of the new coding models on a daily basis for fun and for production code. Here are some of my findings that helped me reduce iterations with my agents.</p><p><strong>Core Insight</strong></p><p>To get the best out of your LLM agents:</p><ol><li><p><strong>Make it easy to write good code</strong></p></li><li><p><strong>Make it hard to write bad code</strong></p></li></ol><p>Let&#8217;s explore what this means in practice.</p><h2>Making It Easy to Write Good Code</h2><h3>1. Write Code in Predictable Ways</h3><blockquote><p>LLMs are pattern-matching machines. When your codebase has consistent patterns, LLMs can accurately replicate them.</p></blockquote><p><strong>What this means in practice:</strong></p><ul><li><p>The way you write logs should be standardized across the codebase</p></li><li><p>Configuration handling should follow the same pattern everywhere</p></li><li><p>API endpoint naming should be predictable</p></li><li><p>Folder structures should be consistent</p></li><li><p>File naming should match what&#8217;s inside</p></li></ul><p><strong>Example Scenario</strong>:<br>If you have 10 controllers that all follow the same structure - constructor, private methods, public methods, error handling - when an LLM creates the 11th controller, it will naturally follow the same pattern.</p><p><strong>Why it works</strong>:<br>LLMs learn from the code they see. Consistent patterns mean the LLM has strong signals about how to write new code. Inconsistent patterns mean the LLM has to guess, and guesses lead to errors.</p><h3>2. Have Reference Files for Coding Styles</h3><blockquote><p>One good example is worth a thousand words of explanation.</p></blockquote><p><strong>What to include:</strong></p><ul><li><p>A well-written test file that shows your testing patterns</p></li><li><p>A reference controller that demonstrates your preferred structure</p></li><li><p>Example configuration files that show the right way to set things up</p></li><li><p>Sample pipeline definitions if you use CI/CD</p></li></ul><blockquote><p>Make sure the reference file is <strong>of high quality</strong>. A bad reference file is worse than no reference file.<br>The LLM will replicate the problems.</p></blockquote><h3>3. Create Base Documentation for Project Structure</h3><blockquote><p>LLMs need to understand the big picture before they can work on specific pieces.</p></blockquote><p><strong>Create a document that answers:</strong></p><ul><li><p>What does this project do?</p></li><li><p>How is the code organized? (What goes where?)</p></li><li><p>What are the key conventions we follow?</p></li><li><p>What external systems do we integrate with?</p></li><li><p>What&#8217;s our approach to error handling?</p></li></ul><p>This doesn&#8217;t need to be elaborate. A simple README or ARCHITECTURE.md file that gives the 30,000-foot view is enough.</p><p><strong>Why it matters</strong>:<br>Without this context, LLMs either load a lot of context by scanning through files or make assumptions. With this context, they make informed decisions.</p><h3>4. Make Similar Things Look Similar</h3><blockquote><p>If two things do similar jobs, they should look similar in code.</p></blockquote><p>This is especially powerful for:</p><ul><li><p><strong>Pipeline definitions</strong> - Infrastructure-as-code files like Bicep, ARM templates, GitHub Actions, or Azure DevOps YAML should follow templates</p></li><li><p><strong>API endpoints</strong> - If you have 20 REST endpoints, they should all follow the same pattern for routing, validation, and error handling</p></li><li><p><strong>Database queries</strong> - Query patterns should be consistent (parameterized, error handling, connection management)</p></li></ul><p><strong>What happens when you do this</strong>:<br>LLMs become extremely good at replicating well-defined patterns. If your pipeline files all look alike, the LLM can create new ones with high accuracy.</p><h3>5. Provide Examples in Requirements</h3><blockquote><p>Task descriptions with examples produce better results than task descriptions with just text.</p></blockquote><p>&#10060; <strong>Instead of:</strong></p><pre><code>Add validation for the email field
</code></pre><p>&#9989; <strong>Write:</strong></p><pre><code>Add validation for the email field.

Example of how we validate in UserController.cs:
- Check if email is not null
- Check if email matches regex pattern
- Return ValidationError if check fails

Follow the same pattern we use for phone number validation in the same file.
</code></pre><p><strong>Why it works</strong>:<br>LLMs are one-shot generators for most PR tools. They don&#8217;t get to iterate with you. More context upfront = better first attempt.</p><h3>6. Keep Changes Small and Focused</h3><blockquote><p>One change per PR works better than multiple changes in one PR.</p></blockquote><p>LLMs are getting exceedingly good at making large changes in one shot. However, the more changes that are made, the more you have to review. This applies to both human-written and LLM-written code, but it&#8217;s especially important for LLMs because:</p><ul><li><p>Larger scope has more chances of deviations from objective.</p></li><li><p>Small, hard-to-catch bugs can seep in when a lot of files are changed.</p></li></ul><p><strong>What to avoid</strong>:<br>&#8220;Refactor the entire service layer and add three new features&#8221; is too much for one task.</p><p><strong>What works better</strong>:<br>&#8220;Add email validation to UserController following the pattern in UserValidator.cs&#8221;</p><blockquote><p>It also works fine if you give a structured list of changes in multiple files to steer the direction of changes.</p></blockquote><h2>Making It Hard to Write Bad Code</h2><p>These are guardrails that prevent LLMs (and humans) from making common mistakes.</p><h3>1. Use Linters and Code Analysis Rules</h3><blockquote><p>Most LLM-based coding agents use builds as a way to validate their changes. If the build fails, they fix and retry.</p></blockquote><p>This means linters are incredibly powerful for steering LLM behavior.</p><p><strong>What to enforce:</strong></p><ul><li><p>Code style rules (consistent formatting, naming conventions)</p></li><li><p>Code analysis rules (potential bugs, security issues)</p></li><li><p>Static analysis (unused variables, unreachable code)</p></li><li><p>Custom rules specific to your domain</p></li></ul><p><strong>&#128161;Tip</strong></p><p>If something is important enough that you comment on it during code reviews, make it a linter rule instead.</p><p><strong>Why this is powerful</strong>:<br>Linters give immediate feedback. LLMs see the error, understand what&#8217;s wrong, and fix it automatically. This creates a tight feedback loop that improves code quality without human intervention.</p><h3>2. Break Builds on Violations (Not Just Warnings)</h3><blockquote><p>Some LLMs ignore warnings. They only fix errors that break the build. If something is important, make it an error, not a warning.</p></blockquote><p><strong>Example:</strong></p><ul><li><p>Security rule violations &#8594; Break the build</p></li><li><p>Required documentation missing &#8594; Break the build</p></li><li><p>Code coverage below threshold &#8594; Break the build</p></li><li><p>Naming convention violations &#8594; Break the build</p></li></ul><p><strong>The tradeoff</strong>:<br>This might seem strict, but it&#8217;s actually liberating. It makes the rules explicit and automatic. Nobody has to remember to check; the build checks automatically.</p><h3>3. Avoid Suppression Files, Use Inline Suppressions</h3><blockquote><p>Some LLMs use global suppression files as an escape hatch when problems become complex.</p></blockquote><p><strong>What happens with suppression files</strong>:<br>Sometimes, when the LLM can&#8217;t fix a code analysis error, it adds a suppression to a global file and moves on. This accumulates technical debt.</p><p><strong>Better approach</strong>:<br>Require inline suppressions with justification:</p><pre><code>#pragma warning disable CA1031 // Do not catch general exception types
// We need to catch all exceptions here because this is the top-level error handler
catch (Exception ex)
{
    // handle
}
#pragma warning restore CA1031
</code></pre><p><strong>Why this works</strong>:<br>Inline suppressions are visible during code review. They force the developer (human or AI) to justify why the rule doesn&#8217;t apply in this specific case.</p><h3>4. Have Tests that LLMs Can Follow</h3><blockquote><p>If you have one well-written test, LLMs can generate more tests following the same pattern.</p></blockquote><p><strong>What makes a good reference test:</strong></p><ul><li><p>Clear arrange-act-assert structure</p></li><li><p>Descriptive test names that explain what&#8217;s being tested</p></li><li><p>Good use of test helpers and fixtures</p></li><li><p>Appropriate assertions</p></li></ul><p><strong>Instruction that works well</strong>:</p><pre><code>Add tests for the new validation logic.
Follow the pattern in UserValidationTests.cs.
Use the same helper methods and assertion style.
</code></pre><p><strong>What to avoid</strong>:<br>Don&#8217;t let LLMs add unnecessary tests just to increase coverage. Specify &#8220;add relevant tests only&#8221; in your requirements.</p><h2>Practical Tips for Faster Iterations</h2><h3>1. Set Up Quick Validation Builds</h3><blockquote><p>If your build takes 20 minutes and the LLM runs 5 iterations, the time just adds up.</p></blockquote><p><strong>The Solution</strong>: Create a lightweight &#8220;quick validation&#8221; build specifically for LLM iterations:</p><ul><li><p>Skip time-consuming steps that aren&#8217;t relevant (deployment, packaging, slow integration tests)</p></li><li><p>Run only linters, unit tests, and fast validations</p></li><li><p>For agent-based PRs, save the full build for final PR validation and use the simpler build before agents publish the PR.</p></li></ul><h3>2. Add Instructions to Avoid Common Issues</h3><blockquote><p>LLMs sometimes add things you don&#8217;t want. Be explicit about what NOT to do.</p></blockquote><p><strong>Examples of useful negative instructions:</strong></p><pre><code>- Don't add suppression files unless absolutely necessary
- Don't add markdown files for explanation
- Don't add unnecessary comments
- Don't add tests that just verify implementation details
</code></pre><p><strong>Why this matters</strong>:<br>LLMs often try to be &#8220;helpful&#8221; by adding extra documentation or comments. If you don&#8217;t want that, say so explicitly.</p><h2>Continuous Improvement Strategy</h2><p>You don&#8217;t need to fix everything at once. Here&#8217;s a gradual approach based on what has the highest impact:</p><h3>Phase 1: Foundation (Week 1-2)</h3><ol><li><p>&#9989; Create a README with project structure and conventions</p></li><li><p>&#9989; Identify and document your most common patterns (logging, error handling, configuration)</p></li><li><p>&#9989; Create reference files for common tasks (tests, controllers, services)</p></li></ol><h3>Phase 2: Guardrails (Week 3-4)</h3><ol><li><p>&#9989; Enable linters and code analysis rules</p></li><li><p>&#9989; Make important rules break the build (not just warnings)</p></li><li><p>&#9989; Remove global suppression files; require inline suppressions</p></li></ol><h3>Phase 3: Optimization (Ongoing)</h3><ol><li><p>&#9989; Set up quick validation builds for faster iterations</p></li><li><p>&#9989; Test with LLMs and refine based on results</p></li><li><p>&#9989; Add instructions files like copilot-instructions and claude skills to avoid common issues</p></li></ol><blockquote><p>Start with the area of your codebase that changes most frequently. That&#8217;s where you&#8217;ll see the biggest return on investment.</p></blockquote><h2>Hacks and Tricks from the Field</h2><h3>Use Voice Typing for Task Descriptions</h3><p>Adding context is important, but typing detailed task descriptions is tedious. Use voice typing to:</p><ul><li><p>Add more detail without typing fatigue</p></li><li><p>Provide richer context naturally</p></li><li><p>Explain examples and patterns verbally</p></li></ul><p><strong>Why it works</strong>: People naturally provide more context when speaking than when typing.</p><h3>Have a Copilot Instructions File</h3><p>Create a <code>.github/copilot-instructions.md</code> file (or similar) that tells LLMs:</p><ul><li><p>What patterns to follow in your codebase</p></li><li><p>What to avoid</p></li><li><p>How to structure code</p></li><li><p>What good looks like</p></li></ul><p>This file is picked up by Copilot and other tools, reducing the need to repeat instructions in every task.</p><h2>Key Takeaways</h2><ol><li><p><strong>Treat LLMs like new team members</strong> - The same things that help humans also help AI</p></li><li><p><strong>Consistency beats perfection</strong> - A consistent mediocre pattern is better than inconsistent excellent patterns</p></li><li><p><strong>Make good code easy</strong> - Clear patterns guide LLMs toward quality</p></li><li><p><strong>Make bad code hard</strong> - Linters and build rules prevent common mistakes</p></li><li><p><strong>Provide examples</strong> - One good reference file beats long written instructions</p></li><li><p><strong>Be explicit</strong> - Say what you want AND what you don&#8217;t want</p></li><li><p><strong>Iterate based on results</strong> - Learn what prompts work and refine your approach</p></li><li><p><strong>Start small</strong> - Fix high-change areas first, then expand gradually</p></li></ol><h2>Conclusion</h2><p>Making your codebase LLM-friendly isn&#8217;t about writing code for robots. It&#8217;s about writing clearer, more maintainable code that communicates its intent effectively to anyone reading it - human or AI.</p><p>The practices in this post are fundamentally about good software engineering: consistent patterns, clear documentation, automated quality checks, and explicit conventions. These practices have always made codebases better for humans. LLMs just make the benefits more obvious and immediate.</p>]]></content:encoded></item></channel></rss>