<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Javeed Shaik - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Javeed Shaik - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 12 Oct 2026 18:31:13 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/javeedshaik7/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ What to Do When Your Constants Are Someone Else's Law ]]>
                </title>
                <description>
                    <![CDATA[ A colleague once asked me why a number in my code had a comment three lines long. The number was 18. The comment said which rule it came from, which section notified it, and that it could change witho ]]>
                </description>
                <link>https://www.freecodecamp.org/news/what-to-do-when-your-constants-are-someone-else-s-law/</link>
                <guid isPermaLink="false">6acb97b4d2b7bc0f3bd063ec</guid>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Code Quality ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Go Language ]]>
                    </category>
                
                    <category>
                        <![CDATA[ fintech ]]>
                    </category>
                
                    <category>
                        <![CDATA[ regulatory compliance ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Javeed Shaik ]]>
                </dc:creator>
                <pubDate>Sun, 11 Oct 2026 14:05:40 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/d0b15ef7-16e3-4521-afe8-f2bd08611f97.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A colleague once asked me why a number in my code had a comment three lines long. The number was <code>18</code>.</p>
<p>The comment said which rule it came from, which section notified it, and that it could change without warning. That felt like a lot of ceremony for a two-digit integer — until I remembered the alternative.</p>
<p>In most software, a constant is a decision you made. In software that implements regulation, a constant is a decision <em>someone else</em> made, which they can change without telling you, and which has a legally correct value you may not be reading correctly in the first place.</p>
<p>I learned both halves of that the hard way, building a set of finance calculators. Here's what went wrong, and the three habits I now apply to any code that encodes a rule someone else owns.</p>
<p>This article is for developers who write code that implements rules somebody else owns, like tax, payroll, benefits, lending, or any domain where a regulator or standards body sets the numbers and can revise them without asking you. If you've ever hard-coded a rate that later changed, this is for you.</p>
<h3 id="heading-prerequisites">Prerequisites:</h3>
<p>You'll need enough JavaScript and Go to read short snippets. Both appear below, but only as illustration. Nothing here depends on knowing either language deeply.</p>
<p>I don't assume any finance or tax background, and I'll explain every domain term where it first appears. The examples come from Indian GST because that's where I hit these bugs, but none of the three failure modes are specific to India, to tax, or to any one jurisdiction.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-the-bug-that-returns-a-plausible-number">The Bug That Returns a Plausible Number</a></p>
</li>
<li><p><a href="#heading-the-constant-that-changes-while-you-sleep">The Constant That Changes While You Sleep</a></p>
</li>
<li><p><a href="#heading-the-validation-that-lived-in-the-form">The Validation That Lived in the Form</a></p>
</li>
<li><p><a href="#heading-the-three-habits">The Three Habits</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ul>
<h2 id="heading-the-bug-that-returns-a-plausible-number">The Bug That Returns a Plausible Number</h2>
<p>Let's start with the worst kind, because it doesn't look like a bug at all.</p>
<p>In India, if you pay Goods and Services Tax late, you owe interest. The obvious implementation reads: take the tax owed, multiply by the annual rate, and scale by days late.</p>
<pre><code class="language-js">const interest = taxOwed * (ratePct / 100) * (days / 365);
</code></pre>
<p>That code is arithmetically perfect. It is also, in a large number of real cases, several times too high.</p>
<p>The reason is that "the tax owed" isn't one number. A GST return has a gross output liability: the total tax on everything you sold. But most businesses settle much of that with input tax credit, the tax they already paid on their own purchases. Only the remainder is paid in cash. And interest, under the rule that governs it, runs on <strong>the tax actually paid in cash</strong>, not on the gross liability.</p>
<p>So we have the same formula, same rate, and two different inputs. And one of them is wrong:</p>
<pre><code class="language-plaintext">90 days late at 18% p.a.
  on cash actually paid   (₹50,000)    →   ₹2,219
  on gross liability      (₹3,00,000)  →  ₹13,315
</code></pre>
<p><strong>Six times the interest.</strong> Both numbers are what the formula returns. Neither throws. Neither looks obviously absurd. If you're the developer, your tests pass, because you wrote the tests against the same misunderstanding as the code.</p>
<p>This is the failure mode that makes regulatory code different. A regular bug produces a stack trace, or a number so wrong that someone notices. This one produces a number that is the right shape, the right order of magnitude, and confidently incorrect.</p>
<h3 id="heading-what-actually-fixes-it">What Actually Fixes it</h3>
<p>A better test doesn't fix this. A better doc comment does.</p>
<p>The fix is to write down, at the definition site, <em>which</em> quantity the parameter means and where that requirement comes from:</p>
<pre><code class="language-go">// GSTInterest returns interest on a late GST payment.
//
// Under Rule 88B(1) interest runs on the tax actually debited from the
// electronic cash ledger — NOT on the gross output liability. Getting
// this wrong overstates the interest, often by several times. Pass
// cashTaxPaid as the cash-ledger portion, not the gross bill.
//
// Pass annualRatePct as DefaultGSTInterestRate unless the notified rate
// has moved; 18% p.a. is currently notified under Section 50(1).
func GSTInterest(cashTaxPaid float64, days int, annualRatePct float64) float64
</code></pre>
<p>Notice that the parameter is called <code>cashTaxPaid</code>, not <code>amount</code>. That name is the entire defense. A caller who has the gross bill in hand has to stop and ask whether it's the right number (which is exactly the pause that prevents the bug).</p>
<p>I'd go further: <strong>if a parameter can be confused with a similar quantity, the name should make the confusion impossible.</strong> <code>amount</code> invites the mistake. <code>cashTaxPaid</code> forbids it.</p>
<h2 id="heading-the-constant-that-changes-while-you-sleep">The Constant That Changes While You Sleep</h2>
<p>The second failure mode is slower and more embarrassing.</p>
<p>Tax brackets move with the annual budget. Statutory ceilings get revised by notification. A gratuity limit sits at one figure for years and then doesn't. None of these changes care about your release schedule.</p>
<p>The tempting implementation is a constant:</p>
<pre><code class="language-go">const gratuityCeiling = 2000000
</code></pre>
<p>Now think about who's hurt by that. Not only the user reading a stale number, but also the <em>developer</em> who spots the change on a Tuesday, finds the value baked into a dependency, and can do nothing but file an issue and wait.</p>
<p>There's also a subtler problem. Statutory figures often aren't one value. They're one value <em>for most people</em>. The same ceiling is higher for certain government employees. The same minimum service period is shorter for fixed-term staff. A hidden constant isn't just potentially stale. It's silently wrong for an entire class of user, forever.</p>
<p>So: make statutory values <strong>options with documented defaults</strong>, never hidden constants.</p>
<pre><code class="language-go">// GratuityOptions carries the statutory figures that change by
// notification. The zero value means "use the defaults".
type GratuityOptions struct {
    Ceiling  float64 // 0 -&gt; 20,00,000. Use 25,00,000 for Central Government civil employees.
    MinYears float64 // 0 -&gt; 5. Use 1 for fixed-term employees.
}
</code></pre>
<p>The zero value still does the right thing for the common case, so nobody pays a complexity tax to get started. But the day the figure moves, a caller can correct it themselves – immediately, without waiting for me to cut a release.</p>
<p>That last clause is the real point. When your code encodes a rule you don't control, <strong>your users need an override that doesn't route through your release cadence.</strong></p>
<h2 id="heading-the-validation-that-lived-in-the-form">The Validation That Lived in the Form</h2>
<p>The third one caught me during a refactor, and it's the most transferable.</p>
<p>I had a function that builds a loan amortisation schedule: one row per month, stepping through the tenure. Inside an app, this was safe. The form had a max attribute. The UI clamped the input. Nobody could ask for a 200-year loan because the interface wouldn't let them.</p>
<p>Then I extracted the calculations into a library, and the form stopped existing.</p>
<pre><code class="language-go">rows, _ := Amortisation(principal, rate, months) // months came from where?
</code></pre>
<p>The loop is bounded only by the tenure it's handed. With a nonsense value, it doesn't return a wrong answer or run slowly. It just allocates until the <strong>caller's</strong> process dies. My bug, their out-of-memory crash, in a program I've never seen.</p>
<p>The fix is unglamorous: put the guard in the function.</p>
<pre><code class="language-go">const MaxMonths = 1200 // 100 years

rows, err := Amortisation(principal, rate, 2000)
// err: months must be at most 1200, got 2000
</code></pre>
<p>The ceiling is deliberately absurd. The longest home loan actually sold runs 30 to 40 years, so 100 years only ever fires on input that was never going to mean anything. That's the property you want in a guard like this: <strong>it should be impossible to hit by accident, so it never becomes a limit anyone argues with.</strong></p>
<p>The general lesson is bigger than one function. When you extract a library out of an application, you inherit every input validation the UI was quietly doing for you, and you inherit it as <em>someone else's</em> crash.</p>
<h2 id="heading-the-three-habits">The Three Habits</h2>
<p>If you write code that implements rules you didn't write, there are three habits you should adopt now:</p>
<h3 id="heading-1-name-the-authority-in-the-doc-comment">1. Name the Authority in the Doc Comment.</h3>
<p>Not "the rate". <em>Which</em> rule, <em>which</em> section, and that it's subject to change. The comment above names Rule 88B(1) and Section 50(1) precisely so that anyone checking my work knows what to check it against, and so that a reader who disagrees can go and read the source rather than trusting me.</p>
<h3 id="heading-2-make-statutory-values-options-with-defaults-not-constants">2. Make Statutory Values Options with Defaults, Not Constants.</h3>
<p>The zero value handles the common case. The override handles the day the law moves, and the class of user the default was never right for.</p>
<h3 id="heading-3-move-the-guard-from-the-form-to-the-function">3. Move the Guard from the Form to the Function.</h3>
<p>Any validation the UI is doing is validation your library isn't. Assume the next caller has no UI at all.</p>
<p>None of this is exotic. It's the ordinary discipline of not trusting your inputs, applied to a domain where the <em>definitions</em> of the inputs are set by someone else and revised on their timetable.</p>
<p>The reason it's worth naming separately is that domain's specific failure signature: no exception, no stack trace, and no obviously wrong output. Just a number that's the right shape and six times too large, sitting quietly in a report that someone is about to act on.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>Here's what this comes down to.</p>
<p>Code that implements regulation has a failure signature of its own: no exception or stack trace, just a number that looks right and is confidently wrong.</p>
<p>We saw it three ways: a parameter that silently meant a different quantity than the rule intends, a statutory value frozen into a constant that the law later moved, and validation that stayed behind in the UI when the logic was extracted into a library.</p>
<p>The three habits answer those three failures in order. Name the authority at the definition site, so the next reader knows what to check your work against. Expose statutory values as options with defaults, so the day the law moves you change an argument instead of hunting for a literal. And put the guard in the function, because the next caller may have no UI at all.</p>
<p>If you take one thing away, make it the naming. <code>amount</code> invites the bug. <code>cashTaxPaid</code> forbids it. And a parameter name is the cheapest documentation you will ever write.</p>
<p>Thanks for reading!</p>
<p><em>I build</em> <a href="https://emicalcs.com"><em>EMICalcs</em></a><em>, a set of free finance calculators for India — loans, SIP, GST, gratuity and tax. The formulas above are extracted from it and published as an open-source library, so the arithmetic is auditable by anyone who wants to check my work.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ I Added a CDN Cache to My Site and It Got Slower. Here's the Math I Should Have Done First. ]]>
                </title>
                <description>
                    <![CDATA[ Last week I put a CDN cache in front of a static site, expecting it to get faster. But instead, it got measurably slower. Not subtly: an independent crawler that had flagged 38 slow pages before the c ]]>
                </description>
                <link>https://www.freecodecamp.org/news/cdn-cache-made-my-site-slower/</link>
                <guid isPermaLink="false">6a9088cbfe512a7250d45587</guid>
                
                    <category>
                        <![CDATA[ performance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloudflare ]]>
                    </category>
                
                    <category>
                        <![CDATA[ caching ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Javeed Shaik ]]>
                </dc:creator>
                <pubDate>Thu, 27 Aug 2026 18:58:19 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/0d81b7df-9dbd-4288-80c5-3ea40551fe4a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Last week I put a CDN cache in front of a static site, expecting it to get faster.</p>
<p>But instead, it got measurably slower. Not subtly: an independent crawler that had flagged 38 slow pages before the change flagged <strong>75</strong> after it.</p>
<p>I reverted it the same day. This is what happened, why it happened, and the one calculation that would have told me not to bother before I deployed anything.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-setup">The Setup</a></p>
</li>
<li><p><a href="#heading-what-i-changed">What I Changed</a></p>
</li>
<li><p><a href="#heading-the-result">The Result</a></p>
</li>
<li><p><a href="#heading-why-it-backfired-part-one-a-miss-is-not-free">Why it Backfired, Part One: a MISS is Not Free</a></p>
</li>
<li><p><a href="#heading-why-it-backfired-part-two-crawlers-are-always-cold">Why it Backfired, Part Two: Crawlers Are Always Cold</a></p>
</li>
<li><p><a href="#heading-the-calculation-i-shouldve-done-first">The Calculation I Should've Done First</a></p>
</li>
<li><p><a href="#heading-the-part-that-stings">The Part That Stings</a></p>
</li>
<li><p><a href="#heading-a-technique-worth-keeping">A Technique Worth Keeping</a></p>
</li>
<li><p><a href="#heading-what-id-tell-you-to-do-instead">What I'd Tell You to Do Instead</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You don't need to have run a CDN before. But this will make more sense if you're comfortable with:</p>
<ul>
<li><p><strong>What a CDN does at a high level:</strong> it keeps copies of your pages on servers around the world and serves each visitor from a nearby one.</p>
</li>
<li><p><strong>Basic HTTP caching headers:</strong> roughly what <code>Cache-Control</code> and <code>max-age</code> are for. I explain <code>s-maxage</code> and <code>must-revalidate</code> where they come up.</p>
</li>
<li><p><strong>Reading a</strong> <code>curl</code> <strong>command:</strong> every measurement in this article is a one-line <code>curl</code> you can run against your own site.</p>
</li>
</ul>
<p>No Cloudflare-specific knowledge is assumed, and there's nothing to install. The core lesson is arithmetic, and it applies to any CDN.</p>
<h2 id="heading-the-setup">The Setup</h2>
<p>The site is a static Astro build: 77 HTML pages, no server-side rendering, and no database. It sits behind Cloudflare on the free plan, with an origin server in Navi Mumbai. Traffic is modest: roughly 9 visitors a day.</p>
<p>Google Search Console reported an <strong>average response time of 711 ms</strong>. Google's own guidance is 200 ms. That's 3.5× over, and it looked like an obvious problem with an obvious cause.</p>
<p>The cause was real: Cloudflare was caching <strong>none</strong> of the HTML:</p>
<pre><code class="language-plaintext">$ curl -sI https://example.com/ | grep -i 'cache-control|cf-cache-status'
cache-control: no-cache
cf-cache-status: DYNAMIC
</code></pre>
<p><code>DYNAMIC</code> means Cloudflare isn't caching the response at all. Every single HTML request (from every visitor and every crawler on earth) was traveling all the way to Mumbai.</p>
<p>The origin was sending <code>Cache-Control: no-cache</code>, which was a deliberate choice by past-me so that deploys would be visible immediately. Static assets (JS, CSS, fonts, and images) were cached fine. Only the HTML was passing through.</p>
<p>So: origin far away, HTML uncached, and a metric saying responses were slow. Cache the HTML at the edge and the problem goes away. Right?</p>
<h2 id="heading-what-i-changed">What I Changed</h2>
<p>I changed two things, in this order.</p>
<p><strong>1. The origin header.</strong> From <code>no-cache</code> to:</p>
<pre><code class="language-plaintext">Cache-Control: public, max-age=0, s-maxage=31536000, must-revalidate
</code></pre>
<p>This is a useful pattern worth knowing. <code>max-age=0, must-revalidate</code> means <strong>browsers</strong> revalidate on every request, so a deploy is instantly visible to users, exactly what <code>no-cache</code> guaranteed. But <code>s-maxage</code> applies <strong>only to shared caches</strong>, which lets a CDN hold the object for a year while browsers keep checking.</p>
<p><strong>2. A Cloudflare Cache Rule.</strong> Here's a detail that surprised me: the header alone does nothing. Cloudflare won't cache extensionless HTML on the strength of your <code>Cache-Control</code> header. I verified this on a throwaway path rather than assuming it:</p>
<pre><code class="language-plaintext">req 1: cf-cache-status: DYNAMIC
req 2: cf-cache-status: DYNAMIC
req 3: cf-cache-status: DYNAMIC
</code></pre>
<p>Five requests, with <code>s-maxage</code> set to a year, still <code>DYNAMIC</code> every time. You need an explicit Cache Rule marking the response <em>eligible for cache</em>. That's actually a useful property. It means you can ship the header change safely, well ahead of switching caching on.</p>
<p>With the rule deployed, it worked exactly as intended:</p>
<pre><code class="language-plaintext">req 1: cf-cache-status: MISS
req 2: cf-cache-status: HIT
req 3: cf-cache-status: HIT
</code></pre>
<p>And it looked like a win. Time to first byte on a warm page dropped from ~0.28 s to ~0.17 s.</p>
<p>I was measuring from India. The origin is in India. <strong>Hold that thought.</strong></p>
<h2 id="heading-the-result">The Result</h2>
<p>The next crawl came back worse. Same site, same day, and the same 80 URLs:</p>
<table>
<thead>
<tr>
<th>Crawl</th>
<th>HTML caching</th>
<th>Slow pages flagged</th>
</tr>
</thead>
<tbody><tr>
<td>11:58 AM</td>
<td>off</td>
<td><strong>38</strong></td>
</tr>
<tr>
<td>6:04 PM</td>
<td>on</td>
<td><strong>75</strong></td>
</tr>
</tbody></table>
<p>Nearly double. I had made the exact metric I was trying to fix substantially worse.</p>
<h2 id="heading-why-it-backfired-part-one-a-miss-is-not-free">Why it Backfired, Part One: a MISS is Not Free</h2>
<p>The mental model I had was that caching is a coin flip between two outcomes: a HIT, which is fast, and a MISS, which costs the same as having no cache at all.</p>
<p>That second half is wrong. On a MISS, the CDN doesn't just proxy your response through. It has to <strong>store</strong> the object as it streams. That work costs something.</p>
<p>I measured it by purging the cache and then fetching the same pages two ways: through Cloudflare, and straight to the origin IP with <code>--resolve</code>.</p>
<pre><code class="language-bash"># through Cloudflare, cache cold
curl -s -o /dev/null -w '%{time_starttransfer}' https://example.com/page

# straight to origin, bypassing Cloudflare entirely
curl -sk -o /dev/null -w '%{time_starttransfer}' --resolve example.com:443:203.0.113.10 https://example.com/page
</code></pre>
<p>Across six pages:</p>
<table>
<thead>
<tr>
<th>path</th>
<th>TTFB</th>
</tr>
</thead>
<tbody><tr>
<td>warm HIT</td>
<td><strong>0.239 s</strong></td>
</tr>
<tr>
<td><strong>cold MISS</strong></td>
<td><strong>0.366 s</strong></td>
</tr>
<tr>
<td>straight to origin (no caching)</td>
<td><strong>0.281 s</strong></td>
</tr>
</tbody></table>
<p>A cache MISS was <strong>~85 ms slower</strong> than simply not caching. Which means every request that misses is now <em>worse off</em> than it was before I started.</p>
<h2 id="heading-why-it-backfired-part-two-crawlers-are-always-cold">Why it Backfired, Part Two: Crawlers Are Always Cold</h2>
<p>A cache only pays off when the same URL is requested again while it's still cached, at the same edge location.</p>
<p>A crawler doesn't do that. It fetches each URL <strong>once</strong>.</p>
<p>I could verify this precisely, and the technique is worth stealing: <strong>if a request is served from the CDN's cache, it never reaches your origin.</strong> So the origin access log is a direct measurement of your miss rate.</p>
<pre><code class="language-bash">grep -ic "crawler-user-agent" /var/log/nginx/access.log
</code></pre>
<p>During the crawl of <del>80 URLs, my origin logged **</del>82 requests**. Every single page the crawler asked for was a miss. A <strong>0% hit rate</strong>, and every one of those misses now carried the extra ~85 ms.</p>
<p>The same logic applies to first-time human visitors, who are also, by definition, arriving cold.</p>
<h2 id="heading-the-calculation-i-shouldve-done-first">The Calculation I Should've Done First</h2>
<p>Here's the whole thing, and it takes about a minute.</p>
<p>You have three numbers:</p>
<ul>
<li><p><code>H</code>: time on a cache hit</p>
</li>
<li><p><code>M</code>: time on a cache miss</p>
</li>
<li><p><code>B</code>: your baseline, the time with no caching at all</p>
</li>
</ul>
<p>Caching only wins when your average request beats the baseline:</p>
<pre><code class="language-plaintext">h × H + (1 − h) × M  &lt;  B
</code></pre>
<p>Solve for the hit rate <code>h</code>:</p>
<pre><code class="language-plaintext">h  &gt;  (M − B) / (M − H)
</code></pre>
<p>For my numbers:</p>
<pre><code class="language-plaintext">h &gt; (0.366 − 0.281) / (0.366 − 0.239)
h &gt; 0.085 / 0.127
h &gt; 0.67
</code></pre>
<p><strong>I needed a 67% cache hit rate just to break even.</strong></p>
<p>Now the reality check. 77 pages, and about 9 visitors a day, plus a search crawler refetching each page every few days. Call it 35 HTML requests a day, spread across 77 URLs <em>and</em> across many edge locations worldwide. And a full cache purge on every deploy.</p>
<p>The realistic hit rate is close to zero. I needed 67%.</p>
<p>The change was never going to work, and I could have known that before writing a line of config.</p>
<h2 id="heading-the-part-that-stings">The Part That Stings</h2>
<p>The diagnosis was correct. The origin genuinely is far from most of the traffic. The HTML genuinely wasn't cached. The 711 ms was real.</p>
<p>I just picked a fix without asking <strong>what fraction of requests would actually benefit from it</strong>.</p>
<p>And my verification made it worse, because I measured from a laptop sitting in the same country as the origin. Local <code>curl</code> timings said the change was a success. Only the external crawler – the thing that raised the issue in the first place – showed the regression.</p>
<p>So just make sure you <strong>verify against the metric that flagged the problem, not a proxy for it.</strong></p>
<h2 id="heading-a-technique-worth-keeping">A Technique Worth Keeping</h2>
<p>One piece of this survived the revert, and it's genuinely useful.</p>
<p>If you cache HTML at the edge, your deploy pipeline must purge that cache. And <strong>an API call returning</strong> <code>"success": true</code> <strong>isn't evidence the cache actually dropped anything.</strong> So don't trust it. Check.</p>
<pre><code class="language-bash">LOCAL=$(shasum -a 256 dist/index.html | cut -d' ' -f1)
LIVE=$(curl -s https://example.com/ | shasum -a 256 | cut -d' ' -f1)

if [ "$LOCAL" != "$LIVE" ]; then
  echo "EDGE IS STALE — this deploy is not live" &gt;&amp;2
  exit 3
fi
</code></pre>
<p>That compares the bytes your CDN is actually serving against the bytes you just built. It's the difference between "the purge API said OK" and "the site is genuinely updated."</p>
<p>A related trap: if those hashes <em>never</em> match, suspect a feature that rewrites HTML at the edge (script optimizers, email obfuscation, auto-minification) before you suspect the cache.</p>
<p>Better still, don't hardcode the assumption that caching is on. Ask:</p>
<pre><code class="language-bash">STATE=$(curl -sI https://example.com/ | tr -d '\r' | awk 'tolower($1)=="cf-cache-status:"{print $2}')

case "$STATE" in
  DYNAMIC|BYPASS) echo "HTML isn't cached; a missing purge is harmless" ;;
  *)              echo "HTML IS cached — a failed purge means this deploy is invisible" ;;
esac
</code></pre>
<p>Now the check corrects itself when someone toggles the cache rule, instead of quietly rotting.</p>
<h2 id="heading-what-id-tell-you-to-do-instead">What I'd Tell You to Do Instead</h2>
<p>Calculate your break-even hit rate before you deploy. <code>(M − B) / (M − H)</code>. If you can't plausibly reach it, stop.</p>
<p>Estimate your real hit rate honestly. Requests per day, divided across your URLs, divided again across edge locations, reset on every deploy. It's usually far lower than it feels.</p>
<p>Measure from where your users and crawlers actually are, not from the machine next to your origin.</p>
<p>Remember caching is a popularity bet. It rewards sites where the same URLs are requested repeatedly, in a short window. A high-traffic site would have won here comfortably. A 9-visitors-a-day site never could.</p>
<p>And if you're staring at a slow origin far from your audience, the honest answer might not be a cache at all. It might be moving the origin.</p>
<p><em>I write about the engineering behind</em> <a href="https://healthycalculatorhub.com"><em>Healthy Calculator Hub</em></a><em>, a set of free health and fitness calculators — including, evidently, the optimizations that don't work.</em></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
