D
P
0

WordPress & PHP

Swapped an Image and Video but the Client Still Sees the Old One? `Cache-Control: max-age` Keeps Same-Name Files Cached

July 29, 2026·4 min read
Swapped an Image and Video but the Client Still Sees the Old One? `Cache-Control: max-age` Keeps Same-Name Files Cached

I look after a client site built on WordPress with a custom theme, hosted on Hostinger with LiteSpeed and a CDN in front. Within a few days I hit two incidents that looked unrelated but shared the same root cause. Both had me suspecting the wrong thing first.

The first showed up while I was iterating on the hero video. I re-encoded hero.webm over and over, re-uploaded it under the same filename, refreshed the browser, and the old cut kept playing. I told myself "it is not trimmed yet" more times than I want to admit. For a while I blamed the encode step.

The second came from the client: a photo I had swapped was "not showing". This time I checked what the server was actually sending first.

Tracing it

I inspected the response headers for that photo straight from the server, through the CDN:

curl -I https://client-site.example/wp-content/themes/theme/assets/img/gallery-01.webp

The response showed a perfectly healthy server:

HTTP/2 200
last-modified: Tue, 15 Jul 2026 09:12:44 GMT
content-length: 205431
cache-control: max-age=604800
x-hcdn-cache-status: MISS

Three lines mattered. last-modified was the same day I uploaded, content-length of 205KB matched the new photo, and x-hcdn-cache-status: MISS meant the CDN was not serving from its own cache, it had just pulled a fresh copy from origin. Server side, this was the new file. Yet my browser and the client's both kept showing the old one.

The root cause

The culprit was one line I had waved off as harmless: cache-control: max-age=604800. That is seven days. It means any browser that had opened that photo URL before I swapped it already stored a copy and considered it fresh for a full seven days. During that window the browser will not bother asking the server again. The URL is identical, the filename is identical, so from the browser's point of view there is no reason to re-request.

That is the same-name swap trap: the server can hold the newest file, the CDN can report MISS and fresh, but if the browser holds an unexpired copy for an identical URL, it never sees the new file. This applies equally to img src and video src. My hero video hit the same wall: hero.webm under a fixed name, cached for seven days, so every re-upload was invisible to any browser that had already loaded it.

It also explains why I got bitten hardest: as the heaviest returning visitor, my cache was the fullest. In incognito the new photo and video appeared instantly, since incognito had no old cache for those URLs. That confirmed it: this was not the file, it was the browser cache.

The fix

The answer was not to kill caching. A seven-day max-age is great for performance and I did not want to throw it away. What I needed was a URL that changes whenever the file changes but stays stable while the file stays the same. Browsers cache by the full URL including the query string, so a version query tied to the file's modification time solves both at once.

For the hero video, the urgent one, I appended a ?v=<filemtime> query to the src:

<video src="<?php echo esc_url( $video_url . '?v=' . filemtime( $video_path ) ); ?>" muted playsinline></video>

filemtime() returns the file's modification timestamp. Every time I re-encode and re-upload hero.webm, that timestamp changes, the URL changes, and the browser is forced to fetch again. Without this query the browser keeps serving the old video, and that is not theory, it is exactly what I lived through.

Then I generalized it into one helper for every theme image, so I would not think about it on each swap:

function asset_img( $file ) {
    $path = get_theme_file_path( "assets/img/$file" );
    $url  = get_theme_file_uri( "assets/img/$file" );
    return $url . '?v=' . filemtime( $path );
}

I used asset_img() everywhere: the media and compare arrays on the front page, the heroes and splits in page blocks, and the service and portfolio templates. Now every same-name swap automatically carries a new ?v= and shows up fresh. I verified on staging that new photos loaded with a fresh version like ?v=1782899303.

One deliberate exception: the logo. It almost never changes, so I left it without cache-busting. Cache-busting is for assets you actually swap often, not everything.

During QA, I ran a console snippet via Playwright to force every image to refetch:

[...document.images].forEach(i => {
  const u = new URL(i.src);
  u.searchParams.set('v', 'x');
  i.src = u;
});

Takeaways

  • If the file is correct on the server but the browser still shows an old version, suspect the browser cache, not the file or the CDN.
  • Read the response headers: a large cache-control: max-age plus x-hcdn-cache-status: MISS means the server is fresh while the browser holds a stale copy.
  • Swapping a file under the same name is invisible to anyone who already loaded that URL until max-age expires. This applies to both img src and video src.
  • Do not disable caching to fix this. Use a version query tied to filemtime() so the URL changes when the file changes but stays stable and cached when it does not.
  • Wrap it in one helper used across every swappable asset, so the next swap is fresh automatically.
  • Exclude assets that almost never change, like the logo, so they keep the full cache benefit.