D
P
0

WordPress & PHP

Uploaded Every Patched File but the Site Is Still Broken? LSCache Is Serving Stale HTML (`X-LiteSpeed-Cache: hit`)

July 22, 2026·5 min read
Uploaded Every Patched File but the Site Is Still Broken? LSCache Is Serving Stale HTML (`X-LiteSpeed-Cache: hit`)

A client site I maintain got reported as "completely broken" by its owner one afternoon. The language switcher would not switch, the preloader vanished, the login button errored, the currency switcher sat dead, and some icons refused to render. What confused me: I had already patched every relevant file and uploaded each one to production. Locally everything was correct, checked more than once. But in the client's browser, not a single change showed up. New files on the server, old page in the browser.

My first instinct was the same as everyone's: something must be wrong in my code. So I started patching against theories, and that is where I lost a few hours.

Three rounds of patches that changed nothing

Round one, I suspected the browser side. The client uses Brave, and Brave Shields does like to block certain scripts. I had him disable Shields for the domain. No difference at all.

Round two, I reworked the redirect logic in the language switcher. I figured maybe there was a race condition between the ?lang= redirect and reading the language cookie. I cleaned up the ordering, re-uploaded, asked for a hard refresh. Still zero change.

Round three, I overhauled the entire cookie handling: how setcookie() was called, its path, when the preloader read the state. I fixed all of it and uploaded all of it. The result matched the previous two rounds exactly, which is to say nothing moved on the client's screen.

Three different theories, three different patches, and not one of them mattered, even though I could open the files on the server over SSH and see my new code sitting right there. When the files on disk are correct but what renders stays wrong, and it does not matter how much you patch, the problem is almost never in the code. Something between PHP and the browser is serving an old version.

curl exposed the cause

I stopped guessing and checked what the server was actually sending, not what I assumed it was sending:

curl -I https://client-site.example/ | grep -i x-litespeed-cache

The answer made everything click:

X-LiteSpeed-Cache: hit

That header showed up on every page. The managed host for this site runs LSCache, LiteSpeed's built-in full-page cache. A hit status means the HTML sent to the visitor was not freshly rendered by PHP, it was a stored copy pulled from cache. And that copy predated my uploads. When I viewed the page source, the proof was plain: the homepage markup was still the old version with no preloader element, and main.min.js was still loaded at ?ver=1.6.0, the version from before all my patches.

So my redirect and setcookie() code was correct from the start. The problem was that the code never ran. LSCache intercepted the request first and returned stale HTML from cache before PHP even got a chance to execute.

Why the files on the server are not what the user sees

This is the part that trips people up. A full-page cache stores the entire rendered HTML of a page, then serves it verbatim for the next request without touching PHP again. So the layers you are really dealing with stack up like this:

browser → LSCache full-page cache → PHP/WordPress → files on disk

I was changing the innermost layer, the files on disk. But as long as LSCache still held the old HTML copy and its status was hit, the visitor's request stopped at the second layer and never reached PHP. No amount of uploading will register while the full-page cache does not know its contents are stale. Bumping the theme version, hard refreshing, swapping cookies, all pointless, because they all operate on the wrong layer.

The fix

The fix is four steps, and the core idea is to make LSCache aware that content changed and that it must not serve one identical page to everyone.

First, I bumped the PCP_VERSION constant used to version every asset. With a new version, each asset URL gets a fresh ?ver=, forcing the browser to refetch the JS and CSS instead of reusing the old ones from its own cache.

Second, and this is the key for the switchers, I registered the language and currency cookies as part of the cache key through the litespeed_vary_cookies filter. Without this, LSCache serves one identical HTML for every language and currency value, so switching never becomes visible.

add_filter('litespeed_vary_cookies', fn($c) => array_merge($c, ['pcp_language','pcp_currency']));
 
if ( isset($_GET['lang']) ) {
    nocache_headers();
    do_action('litespeed_control_set_nocache');
}

Third, for requests carrying ?lang= specifically, I called nocache_headers() plus do_action('litespeed_control_set_nocache') so LSCache does not cache that request and the language redirect actually runs in PHP instead of being short-circuited by the cache.

Fourth, as a safety net, I sent a Vary: Cookie header so any cache layer above also treats the cookie as a differentiator.

Takeaways

  • When a client says "still broken after upload," the first move is curl -I on production and check the X-LiteSpeed-Cache header. If it says hit, you are looking at stale HTML.
  • Remember: the files on disk are not what the user sees while a full-page cache sits in front of them.
  • Stop patching code once three different theories all fail to move anything. That signals a cache problem, not a logic bug.
  • Bump the asset version so ?ver= is fresh and the browser refetches JS and CSS.
  • Register any cookie that changes the view (language, currency) with litespeed_vary_cookies so it becomes part of the cache key.
  • Disable caching for requests that must run PHP logic, like ?lang=, with nocache_headers() and litespeed_control_set_nocache.