Bali, Indonesia
Insights

Is LSCache Actually Serving Your WordPress Pages? Check the X-LiteSpeed-Cache Header

November 2, 2026
0:00
You installed the LiteSpeed Cache plugin, switched caching on, and the dashboard shows everything green. The site still takes a noticeable moment before anything appears, so you can't tell whether visitors get a cached page or whether WordPress builds every page from scratch. The plugin settings show what WordPress intends. They don't show what the web server did with a specific request. The X-LiteSpeed-Cache response header does, and once you know how the server reaches that decision, most 'is my cache working' questions have short answers.
LSCache is not a PHP cache that writes HTML files to disk and hopes a rewrite rule picks them up. The cache lives inside the LiteSpeed web server. The WordPress plugin has two jobs: tell the server which responses can be stored and for how long, and tell it what to purge when content changes. The server does the storing and serving. A request moves through these steps:
  1. The request reaches LiteSpeed. If cache lookup is enabled for that site, the server builds a cache key from the hostname, the path, the query string, and any vary values, such as a vary cookie or a mobile flag if mobile caching is on. On LiteSpeed Web Server, the plugin typically enables lookup by writing a CacheLookup on directive into .htaccess inside an IfModule LiteSpeed block. On OpenLiteSpeed, the cache module is configured at server or virtual host level, so the details depend on the setup.
  2. The server checks its cache storage for that key. If a fresh entry exists and the request qualifies (typically a GET or HEAD request), the server sends the stored response with X-LiteSpeed-Cache: hit. PHP never starts, WordPress never loads, and no database query runs.
  3. If no entry exists, the request goes to the PHP handler (lsphp), and WordPress runs normally: plugins load, the theme renders, queries run. At the end, the plugin attaches an instruction header, X-LiteSpeed-Cache-Control, with a value such as public with a max-age, private, or no-cache. It usually adds X-LiteSpeed-Tag as well, which labels the page so a later purge can target it.
  4. The server reads that instruction. If the response is cacheable, the server stores a copy and labels the outgoing response miss. If the response is no-cache, the server passes it through without storing it, and you typically see no X-LiteSpeed-Cache header at all.
The point people most often get wrong: a miss is not a failure. It means the page was cacheable and has just been stored. The real problems look different. You get a miss on every single request, or the header never appears.
What you seeWhat it usually means
X-LiteSpeed-Cache: hitServed from the public cache. PHP did not run.
X-LiteSpeed-Cache: missCacheable, not in cache yet, now stored. The next identical request should hit.
X-LiteSpeed-Cache: hit,privateServed from a per-visitor private cache, for example a logged-in user when private caching is enabled.
No X-LiteSpeed-Cache, but X-LiteSpeed-Cache-Control: no-cacheWordPress ran, and the plugin marked this response uncacheable.
No LiteSpeed headers at allThe request may not have reached LiteSpeed, a proxy stripped the headers, or cache lookup is off.
Whether the X-LiteSpeed-Cache-Control header is visible to the client can depend on the server version and configuration. If you don't see it, its absence alone proves nothing.
A browser is a poor test tool here. It sends cookies, it may be logged in to wp-admin, and extensions or the browser's own cache can blur the result. curl sends a clean request with no cookies, which is exactly what a first-time anonymous visitor looks like. Run the same request twice:
BASH
# -D - prints response headers, -o /dev/null discards the body.
# This sends a GET, like a browser, rather than the HEAD request that -I sends.
for i in 1 2; do
  curl -s -D - -o /dev/null -w 'TTFB: %{time_starttransfer}s
' 'https://example.com/sample-page/' \
    | grep -i -E '^(x-litespeed|set-cookie|server|ttfb)'
  echo '---'
done
Illustrative output format for a healthy page:
TEXT
server: LiteSpeed
x-litespeed-cache: miss
TTFB: 0.900s
---
server: LiteSpeed
x-litespeed-cache: hit
TTFB: 0.050s
---
The timings are illustrative, but the pattern is the useful part. The first request pays for WordPress to run. The second skips PHP entirely, and time to first byte drops sharply. If both requests say miss, look at the set-cookie lines. A response that sets a cookie on every anonymous request is a common reason pages never settle into the cache. If the site sits behind a CDN or another proxy, the headers you see may come from the edge rather than from LiteSpeed. To test the origin directly, add --resolve 'example.com:443:203.0.113.10', using your server's real IP address. curl then connects straight to the origin while still sending the correct hostname and SNI. Query strings matter too, because they are part of the cache key:
BASH
# A new query string usually means a new cache key: expect miss, then hit.
curl -s -D - -o /dev/null 'https://example.com/sample-page/?test=1' | grep -i 'x-litespeed-cache'
curl -s -D - -o /dev/null 'https://example.com/sample-page/?test=1' | grep -i 'x-litespeed-cache'
This is why campaign traffic with unique tracking parameters can produce mostly misses, even when the plain URL is cached. Depending on the plugin version, a setting can ignore specific query parameters, so check your cache settings rather than assuming.
A page generated for a logged-in user contains the admin bar, the user's name, and nonces, which are short-lived security tokens tied to that session. Serving that copy to an anonymous visitor would be wrong, and serving an anonymous copy to an editor would hide their toolbar. For that reason, by default, the plugin doesn't give logged-in users the public cache. The mechanism works through vary values. When WordPress logs someone in, it sets a wordpress_logged_in_ cookie, and the plugin typically sets its own vary cookie (commonly _lscache_vary). On the next request, the server sees that cookie, folds it into the cache key, and the visitor no longer matches the anonymous entries. That request either goes to PHP every time, or, if you enable the plugin's option to cache logged-in users, it is served from a private cache that belongs to that one visitor. That is where hit,private comes from. The same idea applies beyond login. WooCommerce cart cookies put shoppers with items in their cart into a different bucket, and pages like cart, checkout, and account are typically excluded outright. Some plugins start a PHP session or set a cookie for every guest, which can push otherwise ordinary pages into no-cache or a vary that never repeats. When a site never seems to cache, look for a cookie being set on anonymous requests. It often turns out to be the reason. This also explains the most common confusion at a support desk: the site owner tests speed in the same browser where they are logged in to wp-admin. They will never see a public hit there, no matter how well the cache is configured. A private window, or curl, shows what visitors actually get.
There are three situations, and each points somewhere different. If you see a miss on every request, something changes the cache key or the cacheability each time. Common causes are a cookie set on every response, a changing query string, an exclusion rule matching more URLs than intended, or very frequent purges. Some setups purge large parts of the cache whenever a post is saved or a scheduled task runs, so the cache never stays warm for long. The X-LiteSpeed-Cache-Control value, if visible, shows whether the plugin marked the page no-cache or whether the entry was simply gone. If there are no LiteSpeed headers at all, first confirm the request reaches LiteSpeed. The server header helps, though it can be hidden or rewritten. Then test the origin with --resolve to rule out a CDN. If the origin also shows nothing, cache lookup may be disabled at server level. On shared hosting that is a host-side setting, so ask your provider to confirm the cache module is enabled for your account. If you see a hit but the page still feels slow, the HTML is fine and the time goes elsewhere. A hit with a low time to first byte means the server finished its part quickly. What remains is the browser downloading and running images, fonts, CSS, JavaScript, and third-party scripts, and the browser's developer tools waterfall shows that much better than any header. Uncached requests still matter for capacity, though. Every miss starts a PHP process, so a low hit rate under heavy traffic shows up as load on the PHP workers, not only as slow pages. The practical habit is short. Before changing any setting, curl the page twice from a clean session, read X-LiteSpeed-Cache, and look for set-cookie. A miss then a hit means the cache works and the slowness lives elsewhere. A miss twice, or no header at all, tells you which layer to investigate next: cookies and query strings, exclusions and purges, or the server and CDN in front of WordPress.