<?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>Thu, 27 Aug 2026 22:36:33 +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[ 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>
