{"id":"jfmitchklmkxvlf","title":"Cloudflare Cache and Stale Assets After Deploy","slug":"cloudflare-cache-stale-assets-after-deploy","summary":"Cloudflare often serves stale CSS and JS long after a deploy, making it look like the build failed when the CDN is just holding onto old files.  I got…","imageUrl":"https://briancrabtree.me/images/journal-cloudflare-cache-stale-assets-after-deploy.webp","category":"Performance","date":"2026-03-19T18:00:00.000Z","featured":false,"likes":27,"author":"Brian Crabtree","content":"<h2>The Frustration of Stale Assets</h2>\n\n<p>There are few things as frustrating as deploying a fix or feature, only to see the old version stubbornly persist for users. I've wasted too much time debugging \"ghost\" issues that vanish on a hard refresh, making me question my own sanity and the integrity of my build process. This pervasive struggle with external caching layers is a significant source of developer frustration.</p>\n\n<p>The specific problem, where Cloudflare's caching holds onto stale assets after a deploy, has plagued me numerous times. It creates the illusion that your deploy failed or your build system isn't functioning, but often it's just a CDN doing its job too well, blocking critical updates from propagating by aggressively caching content.</p>\n\n<p>Users see outdated CSS, broken JavaScript, or old images that should have updated. Meanwhile, I'm verifying the latest code on the origin server, wondering why the world isn't seeing my changes. This disconnect between successful deployment and actual content delivery demands a precise, automated strategy for consistency and reliability.</p>\n\n<pre><code># Purge hashed assets after deploy — not the whole zone\ncurl -X POST \"https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache\" \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -d '{\"prefixes\":[\"example.com/assets/\"]}'</code></pre>\n\n<p><figure>\n  <img src=\"/images/journal-inline-cloudflare-stale-cache.webp\" alt=\"Diagram of CDN edge serving stale hashed assets after deploy until purge\" width=\"1200\" height=\"675\" loading=\"lazy\" />\n  <figcaption>Hashed assets need cache bust or prefix purge — not hope.</figcaption>\n</figure></p>\n\n<h2>The Core Problem: How Caching Works</h2>\n\n<p>CDNs like Cloudflare sit between your server and the user, caching static content at edge nodes closer to them. This speeds delivery and reduces server load. Cloudflare decides what to cache and for how long based on HTTP Cache-Control and Expires headers from your origin, dictating caching behavior for all intermediaries, including the CDN and the user's browser.</p>\n\n<p>If your server tells Cloudflare to cache a file for a week (e.g., max-age=604800), it typically honors that, serving it globally. While great for stable assets, this is awful for rapid updates. Deploying a new style.css means Cloudflare might serve the old, cached version for days, completely bypassing your intended update.</p>\n\n<p>Browsers also maintain their own local caches, adding another layer of complexity. Even if Cloudflare gets the new file, a user's browser might still display an even older, locally cached version. Understanding this multi-layered caching hierarchy – origin headers, CDN rules, and browser behavior – is fundamental to effective debugging and fixing.</p>\n\n<h2>My Setup and What Broke</h2>\n\n<p>For briancrabtree.me, I run a static site generated by a custom build process, deployed to a simple web server. Cloudflare handles DNS, SSL, and crucially, caching. My CI/CD pipeline pushes new builds to the server on every successful commit to main, ensuring a rapid iteration cycle.</p>\n\n<p>The breaks typically occurred with CSS or JavaScript updates. I'd commit, the pipeline would finish, and files would be on the server. But checking the live site, visual changes weren't present. A direct curl to my origin showed the new file, but one through Cloudflare's network stubbornly returned the old.</p>\n\n<p>I once shipped a critical UI fix that just wouldn't show up for an hour. It was frustrating, professionally embarrassing, and reflected poorly on site reliability. This experience taught me that simply pushing code isn't enough; we must actively manage the entire content distribution pipeline.</p>\n\n<h2>Initial Missteps and Half-Measures</h2>\n\n<p>My first reaction was often to \"Purge Everything\" in the Cloudflare dashboard. While this works by forcing all edge nodes to re-fetch, it's a blunt instrument with significant performance drawbacks. Temporarily, it kills the CDN's primary benefit, as all subsequent user requests become \"MISS\" hits, requiring a full origin round trip.</p>\n\n<p>I also tried appending cache-busting query strings like ?v=123 to filenames, hoping to force a fresh fetch. This approach is notoriously inconsistent across different browsers and intermediate proxies. Furthermore, it's tedious to manage manually, and if not handled carefully, can lead to duplicate content issues and unnecessary server load.</p>\n\n<p>These initial methods felt less like robust solutions and more like desperate workarounds. They addressed the immediate symptom of stale content but failed to tackle the underlying cause: a mismatch between my rapid deployment strategy and Cloudflare's default caching behavior. I needed a more targeted, automated approach.</p>\n\n<h2>Strategic Purging and Cache Control</h2>\n\n<p>The pivotal shift was precisely telling Cloudflare which files had changed. Cloudflare's API offers granular control over its cache, enabling purging specific URLs or entire directories. This programmatic control is infinitely more efficient and less disruptive than a blanket \"Purge Everything.\" My build process now identifies modified files, sending targeted purge requests to the Cloudflare API.</p>\n\n<p>For HTML files, which serve as the entry point, I implemented Cache-Control: max-age=0, must-revalidate at the origin. This crucial directive instructs both Cloudflare and browsers to always check with the origin server before serving cached HTML. This guarantees the main page and its references to other assets are always current.</p>\n\n<p>Conversely, for truly static assets like images or fonts, I continue to leverage longer cache times. My site's custom build process, detailed in <a href=\"/journal/how-this-site-is-built/\">How This Site Is Built (Reference Stack)</a>, automatically applies these tailored HTTP headers based on asset type, striking a critical balance between optimal performance and immediate content freshness.</p>\n\n<h2>Versioning and Fingerprinting Assets</h2>\n\n<p>The most robust and elegant solution I adopted for CSS and JavaScript files is asset versioning, or fingerprinting filenames. Instead of style.css, my build tools now output a unique filename like style.a1b2c3d4.css. The critical principle here is immutability: if the content changes, its cryptographic hash also changes, creating a new and unique filename.</p>\n\n<p>This allows Cloudflare, and any caching layer, to cache these fingerprinted files indefinitely, often with Cache-Control: public, max-age=31536000, immutable headers for a year or more. Indefinite caching is safe because the content under that specific filename will never change. When I deploy an update, the HTML simply references the new filename.</p>\n\n<p>This sophisticated strategy completely solves the problem of CDN serving old files after an update without requiring aggressive purging. It ensures users consistently receive the correct, latest version of assets every time they visit the site, with zero manual intervention. My build tooling automatically handles the fingerprinting, making it a \"set it and forget it\" part of the automated deployment workflow.</p>\n\n<h2>What I Do Next</h2>\n\n<p>My current strategy is an integrated system that combines aggressive Cache-Control for HTML, targeted Cloudflare API purging for critical updates, and comprehensive asset fingerprinting for static assets. This multi-faceted approach ensures immediate visibility of updates while maintaining strong caching performance, resulting in a highly reliable and performant content delivery workflow.</p>\n\n<p>My CI/CD pipeline now includes an explicit step to invoke the Cloudflare API for cache purging. If any HTML pages are directly modified, the pipeline automatically sends targeted requests to cloudflare purge cache deploy for those specific URLs. This small, yet incredibly powerful, automated step has virtually eliminated all instances of stale content persisting unexpectedly.</p>\n\n<p>Building robust and resilient deployment pipelines is crucial for consistent performance, maintaining a positive user experience, and safeguarding the credibility of your online presence. For those interested in delving deeper into the technical specifics of this site's deployment, caching strategies, and custom tooling, I highly recommend exploring <a href=\"/journal/how-this-site-is-built/\">How This Site Is Built (Reference Stack)</a>.</p>","tags":["cloudflare","cdn","caching"],"views":87}