Page Cache Detected but Server Response Time Is Still Slow

Page Cache Detected but Server Response Time Is Still Slow - MakeWPFast

Your caching plugin shows a green badge, PageSpeed says the homepage responds in 300ms, and Site Health still slaps you with a critical issue: “Page cache is detected but the server response time is still slow.” I have seen this confuse a lot of people, including me the first time. The plugin says caching works. The check says the server is slow. Both can be true at once, and the reason is that the check does not measure what most people think it measures.

I read the actual test code to sort this out. Here is what is going on and how to fix the real cause instead of the red label.

What the Site Health check actually does

The test is get_test_page_cache() in WP_Site_Health, added in WordPress 6.1 (November 2022). That is why the message “suddenly” appeared on sites that had been fine for years. The site did not get slower. The measurement just did not exist before.

The mechanics, verified against core source:

  • It makes three requests to your homepage with wp_remote_get( home_url('/') ) and takes the median response time.
  • The threshold is 600ms, filterable via site_status_good_response_time_threshold, and the comparison is strict – a median of exactly 600 already fails. The code comments cite web.dev's TTFB guidance as the basis.
  • “Page cache detected” means one of two things: your site has advanced-cache.php with WP_CACHE enabled (the drop-in most caching plugins install), or any of the three responses carried a recognized caching header. Presence alone is enough for last-modified, etag, and via. The rest check the value: max-age above zero in cache-control, a future expires, a numeric age above zero, x-cache-enabled: true, x-cache-disabled set to anything but on, x-srcache-store-status: store, x-varnish starting with two numbers, or the word HIT in cf-cache-status, x-cache, x-litespeed-cache, x-srcache-fetch-status, x-cache-status, or x-proxy-cache.

Two details matter here.

First, the requests are a loopback: your server calls its own public hostname and times itself. What gets measured is the path from your PHP process back through DNS, TLS, nginx or Apache, PHP-FPM, and your cache layer. A visitor in Warsaw or Dallas takes a different route entirely.

Second, “detected” does not mean a cached page was served. Some of those headers are presence-only. A site with mod_expires sending expires on HTML, or a CDN adding via, counts as “page cache detected” even if nothing is actually caching pages. Detection is a header sniff.

First find out if it is slow for real visitors

Before touching anything, measure the public path:

curl -s -o /dev/null -w 'lookup: %{time_namelookup}s | connect: %{time_connect}s | tls: %{time_appconnect}s | ttfb: %{time_starttransfer}s | total: %{time_total}s\n' https://yoursite.com/

Run it three to five times. On the second run, look at the response headers too (curl -sI) for x-cache: HIT, cf-cache-status: HIT or whatever your cache emits.

Now interpret:

  • Public TTFB comfortably under 600ms, Site Health red. The loopback path is the problem, or the check caught a bad moment. Your visitors are fine; the check is measuring a different route.
  • Public TTFB over 600ms on repeat views. Real problem. The cache is not serving HITs, or the box itself is slow.
  • First request slow, second fast. Cold cache. Normal, but check why the cache keeps expiring.
  • It flips between green and red when you re-run Site Health. Intermittent slowness – shared hosting contention, or cache expiring mid-test. There is a wp.org thread where public tests showed ~330ms TTFB while Site Health read 2,587ms.

If you have SSH, also run the same curl from the server against its own public hostname. That reproduces what Site Health does. When I ran the test inside a local container, the loopback failed outright – cURL error 7, the container could not reach its own hostname at all, and Site Health reports the “Unable to detect the presence of page cache” variant instead. If your server’s route to itself is broken or slow, the check sees it even when nobody else does.

Why “detected” and “still slow” happen together

The check caught cache misses

Three requests, median. To fail, at least two of the three have to come in at 600ms or slower. That happens when the homepage is excluded from caching, the TTL is short enough that the entry expired between your last visit and the test, or a purge just ran. Some plugins also refuse to cache requests that look different from a normal browser – Site Health sends no cookies and no special flags, so it should be cacheable, but check your plugin’s “never cache” rules for the homepage anyway.

A PHP page-cache HIT still burns a PHP worker

This one is widely missed. A cache plugin using advanced-cache.php still runs inside PHP. PHP-FPM has to accept the connection, spin a worker, run the drop-in, and read the cached file. On an exhausted pm.max_children pool, even a HIT queues behind other requests. Server-level caches – nginx fastcgi_cache, LiteSpeed’s cache, your host’s built-in layer, Cloudflare edge – answer without touching PHP at all. If your PHP workers are the bottleneck, moving the cache below PHP fixes both the check and real traffic.

The loopback route itself is slow

The server resolves its own hostname, opens a TCP connection, does a TLS handshake with itself. Slow internal DNS, hairpin NAT, a missing /etc/hosts shortcut, IPv6 weirdness – all of that lands inside the 600ms budget before WordPress even boots. I have seen hosts where the loopback goes out to the public IP and back through the edge, adding hundreds of milliseconds for no reason a visitor would ever see.

The box is just overloaded

Sometimes the boring answer is right. Shared hosting neighbors, a backup running, a cron spike – if the machine is contended, everything including a cache HIT slows down. This is also why the result is flaky on cheap shared plans. Plesk's own KB suggests switching the PHP handler for exactly this reason.

There is no real page cache at all

Remember, detection is header-based. cache-control: max-age=3600 from a .htaccess rule, or a last-modified header, will flip the label to “detected” while your site still builds every page from scratch. WP Rocket documents that it sets cache-control via .htaccess for the same reason. If the homepage headers show no real HIT marker and no advanced-cache.php exists, the “detected” half of the message is lying to you.

The uncached path is heavy

If the check’s requests miss the cache – for any reason above – they run the full WordPress stack: every plugin, every init hook, the autoloaded options, the queries. That is where the usual suspects live: autoload bloat, slow queries, and no persistent object cache. Worth noting the honest limit though: on a true advanced-cache.php HIT, plugins never load and autoload never runs, so this only explains the MISS path or the loopback hitting PHP.

Fix order that actually works

  1. Reproduce from both sides. Public curl, then server-to-itself curl. If only the loopback is slow, skip to DNS/hairpin territory and stop “optimizing” things that were never broken.
  2. Confirm the homepage is a real HIT. Second request should carry a HIT header. If it shows MISS or BYPASS, fix the exclusion, query-string rules, or enable the plugin’s preload/warmup.
  3. Move page caching below PHP if you can. nginx fastcgi_cache, LiteSpeed, host-level cache, or Cloudflare’s edge cache. A HIT that skips PHP cannot be delayed by a busy worker pool.
  4. Speed up the uncached path. Persistent object cache (Redis beats Memcached for WP in my testing), shrink autoloaded wp_options, fix the top slow queries. This is the part WP Multitool automates – it shows autoload size, object cache status, and the slow query list in one screen if you do not want to dig by hand.
  5. Look at PHP workers and the box. If even external HITs are slow, plugins are innocent. Check pm.max_children against traffic, PHP version, and whether the host is simply oversold.
  6. Check for a cron pileup. WP-Cron spawns on page loads, including the loopback homepage hit the check makes. A due job firing during the test inflates the median. Move cron to a real system crontab if your schedules are heavy.

When the check itself is the problem

A few cases where red comes from the measurement itself:

  • Sites behind HTTP basic auth: the check passes credentials only when you trigger it manually. The weekly scheduled run cannot authenticate and fails its loopback.
  • Hosts where the server cannot reach its own public hostname (containers, weird NAT): you get the “unable to detect” variant, which is also noise.
  • Flaky shared hosting: re-running the check is legitimate. If it is green half the time, intermittent contention is the likely story.

There is also site_status_good_response_time_threshold if you want to change the 600ms bar. I would not. The number is reasonable – web.dev wants TTFB under 800ms for a “good” score, so 600 for a health check is already the stricter bar. Fix the cause.

The check is doing you a favor, just in an unhelpful dialect. Translate it – “your server timed a request to itself at 600ms or slower” – and the path forward gets a lot clearer. If you want the official version, Site Health itself links out to WordPress's caching documentation.

Get WordPress Performance Tips

Join developers and agency owners who get backend optimization strategies, tool releases, and deep-dive guides.

No spam. Unsubscribe anytime. I respect your privacy.