Smart Cache-Busting for JSON and Static Assets Using PHP’s filemtime()

From Xshell Ssh, the free encyclopedia of technology

If your website loads a JSON file via fetch() (e.g., a blog listing from posts.json), the browser may cache it aggressively. You publish a new article, but visitors keep seeing the stale list unless they manually clear their cache — which almost nobody does. The fix is simple: use PHP’s filemtime() to append the file’s last-modified timestamp as a query parameter. This forces the browser to refetch only when the file actually changes, while preserving cache benefits between updates. Below we answer common questions about this technique.

Why does a browser cache a JSON file loaded via fetch, and why is it a problem?

When you use fetch('posts.json') in JavaScript, the browser treats the response like any other web resource. Without an explicit Cache-Control directive, Apache applies a heuristic based on the file’s Last-Modified header. In practice, browsers may keep the JSON cached for several hours or even days. This becomes a problem when you update the JSON (e.g., add a new blog post). Visitors who loaded the page earlier will still see the old list because the browser uses its cached copy. They don’t know they need to refresh, and most never manually clear their cache. The result: your new content goes unnoticed until the cache expires naturally — which could take a long time and frustrate both you and your readers.

Smart Cache-Busting for JSON and Static Assets Using PHP’s filemtime()
Source: dev.to

What is the server-side solution using Cache-Control headers, and what are its drawbacks?

You could add an Apache .htaccess rule to send Cache-Control: no-cache, must-revalidate for posts.json. This tells the browser to always ask the server if the file has changed before using the cached version. While effective, this approach has two major drawbacks. First, it requires the mod_headers module to be enabled, which isn’t guaranteed on shared hosting. Second, even with no-cache, the browser still makes a network round-trip on every page load — it sends a conditional If-Modified-Since request. If the file hasn’t changed, the server responds with 304 Not Modified, which is fast but still consumes bandwidth and latency. This negates the performance benefit of caching for unchanged files.

How does query string cache-busting with PHP’s filemtime() work?

The idea is simple: browsers treat a URL as a new resource whenever the query string changes. By appending a version parameter that reflects the file’s last modification time, you force a refetch only when the file actually changes. In PHP, you use filemtime(__DIR__ . '/posts.json') to get a Unix timestamp. Then generate the URL:

fetch('posts.json?v=<?php echo filemtime(__DIR__ . "/posts.json"); ?>')

When the HTML is served, the query becomes something like posts.json?v=1740268800. As long as you don’t modify posts.json, the exact same URL is used every time, so the browser serves from cache — zero network requests. When you edit the file, the timestamp changes, the URL changes, and the browser fetches the fresh version. It’s a perfect balance: cache when possible, bust when necessary.

Smart Cache-Busting for JSON and Static Assets Using PHP’s filemtime()
Source: dev.to

Why is this better than the no-cache header?

The Cache-Control: no-cache header still forces a server round-trip on every visit. The browser must send a conditional request (with If-Modified-Since) and wait for a 304 Not Modified response. Even though the response body isn’t transferred, the network latency remains. With query string cache-busting using filemtime(), if the file hasn’t changed, the URL is identical, so the browser pulls the resource directly from its local cache — no network request at all. This is more aggressive in the right way: you benefit from full caching when nothing changes, and you bust the cache instantly when something does. For modern websites with fast hosting, the difference may be small, but for high-traffic sites or slow connections, eliminating unnecessary round-trips improves perceived performance and reduces server load.

Can this method be used for CSS and JS files too?

Absolutely. This same technique is the foundation of what modern bundlers like Vite and Webpack do with content hashes (e.g., main.a3f9c2.js). Without a build step, you can achieve the same effect using PHP’s filemtime() for any static asset. For example:

<link rel="stylesheet" href="assets/css/styles.css?v=<?php echo filemtime(__DIR__ . '/assets/css/styles.css'); ?>">
<script src="assets/js/main.js?v=<?php echo filemtime(__DIR__ . '/assets/js/main.js'); ?>"></script>

Every time you modify the CSS or JS file, the timestamp updates, the URL changes, and the browser fetches the new version. This works reliably on standard Apache setups and across all modern browsers. It’s a lightweight, zero-infrastructure way to implement cache-busting without needing a build pipeline.

What are the limitations with intermediate proxies or CDNs?

While the query-string cache-busting technique works well for direct browser communication, some intermediate caches (proxies, CDNs) may ignore the query string when matching cached resources. This means a proxy might serve the old version even though the URL includes a new v parameter. For a standard Apache server serving files directly to browsers, this isn’t an issue. But if you’re behind a CDN like Cloudflare or use a reverse proxy, you should ensure that those systems treat the query string as part of the cache key. Many CDNs have an option to include query strings in the cache key (e.g., Cloudflare’s “Cache Key” settings). Alternatively, you can use filename-based versioning (e.g., renaming the file to styles.v1740268800.css). For most shared hosting setups with direct Apache, query-string busting is safe and effective.