Skip to content
Gaurav TiwariDocs

Diagnose Caching and Purges

Version 1.3.6 Updated October 26, 2026

The tools for answering two questions: why is this page (not) cached, and did that purge actually work. Roughly in the order you should reach for them:

  1. Response headers
  2. Explain This Page
  3. Verified purge receipts
  4. Diagnostic logging
  5. The health report, Site Health and doctor

None of them change settings. Only purge verification changes anything at all (it purges the URL it checks).

Response headers

curl -sSI https://example.com/some-page/ | grep -i '^x-gt-'

X-GT-Cache is set by the drop-in, or by WordPress when the drop-in hands the request on:

ValueMeaningNeeds Diagnostic logging
HITServed from the stored page, still freshNo
STALEServed from the stored page past its fresh lifetime, inside the stale windowNo
MISSNothing usable was stored; WordPress rendered the pageNo
EXPIREDThe stored page is past its stale window; WordPress rendered the pageNo
REVALIDATEA preload request skipped a stale copy on purpose so the page is rebuiltNo
SAFE-MODEGTPERF_SAFE_MODE is on; nothing is served from the cache, stored or transformed (see Troubleshooting)No
BYPASSThe request is not eligible; X-GT-Cache-Reason names the ruleYes
DYNAMICThe request was eligible, but the rendered response failed validation and was not stored; X-GT-Cache-Reason names whyYes
OPTIMIZE-ONLYOptimize-only mode: the page was optimized for the host’s cache and not stored here; X-GT-Cache-Reason is optimize-onlyYes

With Diagnostic logging off, a bypassed request carries no X-GT-Cache header at all, and a response that fails validation keeps the MISS it was given before rendering (its Cache-Control: no-store, private, max-age=0 is the tell). Bypass rules are not advertised to the public unless you turn logging on.

Other headers worth knowing:

HeaderWhen
X-GT-Cache-ReasonWith BYPASS, DYNAMIC or OPTIMIZE-ONLY, only while Diagnostic logging is on. The value is an eligibility reason or a validation reason (status:404, not_html, set_cookie, private_cache_control, content_type, donotcachepage, page-option). The drop-in and WordPress sanitize it with the same rule, keeping letters, digits and _ : / . * - and cutting it at 200 characters, so cookie:wordpress_logged_in_ and path:/cart/ arrive as written
X-GT-Cache-KeyOn a hit served by the drop-in, only while Diagnostic logging is on. The first 12 characters of the cache key hash, the same value as cache_hash_short in Explain This Page
X-GT-Commerce-Cache: BYPASSAlways, on responses a commerce or cache exception keeps dynamic (a bypass path, cookie or query parameter, or a query parameter sent as an array). See Commerce Safety

No X-GT-Cache header on a public page with logging on usually means the page cache is not active: the drop-in is missing or not owned, or WP_CACHE is off. In store mode, capture does not run until both are in place. See Page Cache.

A cache hit also carries the security and indexing headers the page was rendered with (Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Robots-Tag and the rest of the allowlist on Page Cache). A page stored before 1.1.1 lacks them until it is rebuilt.

Behind Cloudflare or another CDN you may be reading an edge copy rather than your origin. Add a throwaway query parameter to force a real origin render. An unknown parameter bypasses the cache on purpose, so the response shows what WordPress renders now, not what is stored.

Explain This Page

wp gt-performance cache explain --page-url=https://example.com/pricing/

It reports, for one URL of this site, how the cache policy treats it. It does not request the URL; it evaluates the current policy and reads the stored entry from disk. Without --page-url it explains the home page. The flag is --page-url because WP-CLI reserves --url.

FieldWhat it tells you
cacheable, reasonThe eligibility decision and the rule that made it, the same reason X-GT-Cache-Reason would show
cache_key, cache_hash, cache_hash_shortThe computed key and its hash; compare cache_hash_short with X-GT-Cache-Key
origin.statefresh, stale, expired or missing for the stored page
origin.bytes, stored_at, fresh_until, stale_untilSize and Unix timestamps of the stored entry
cloudflareWhether the Cloudflare integration is on, the compiled rule expression, whether that rule matches this request, and whether edge and origin agree
policyThe settings generation and how many bypass paths, cookies and query parameters are in force

cloudflare.agrees is false only in the unsafe case: the managed rule matches the request, a positive Cloudflare edge cache lifetime overrides origin freshness, and the origin says the page must not be cached. cloudflare.disagreement then tells you to set the lifetime to 0. See Cloudflare.

The same report is available as:

  • the gt-performance/explain-url ability, which can also inspect the mobile variant. See AI and MCP.
  • the adviser task Explain how this page is cached and optimized, which sends a redacted version of it to your AI provider. See AI and MCP.

In wp-admin, Tools > Explain this page shows the same report for a URL you enter: the decision and reason, the origin copy’s state, when it was stored and how long it stays fresh, its size, the short cache key, and (with Cloudflare connected) whether Cloudflare agrees with the origin. It also links to the page in per-request safe mode (Open this page with every optimization off), which only an administrator can open. Nothing is fetched or purged. The admin bar’s Explain this page item opens this panel with the current page’s URL filled in.

A post with Don’t cache this page ticked is reported as not cacheable with the reason page-option.

Source: CacheInspector.

Verified purge receipts

A purge is easy to believe and hard to check. Purge verification is the difference between “I clicked purge” and “the stored page is gone, and visitors now get a clean response”.

Run it from a front-end page with the admin bar’s Purge and verify this URL, or:

wp gt-performance cache verify --page-url=https://example.com/pricing/

Without --page-url it verifies the home page. For that URL it:

  1. records the stored entry’s state,
  2. purges the URL at the origin and checks that the stored entry is now missing,
  3. sends the queued edge purge and waits for Cloudflare to answer,
  4. requests the public URL twice, the way a visitor would (through your CDN),
  5. saves a receipt.
Receipt fieldMeaning
statusverified when every signal below passes, warning when any does not, failed when the edge purge returned an error
origin_removedThe stored page was gone after the purge
body_stableBoth public responses had the same body fingerprint (a truncated SHA-256; bodies are never stored)
safe_responseBoth responses were HTTP 200 and neither was private (no Set-Cookie, no private/no-store)
first_was_old_hitWith Cloudflare on, the first response was a Cloudflare HIT or STALE with a positive Age, which means the edge still served an old copy
edge_first, edge_secondStatus, byte count, Cache-Control, CF-Cache-Status, X-GT-Cache, Age, and whether a cookie was set, for each response

body_stable is false when the two responses differ, which happens when the page is rendered fresh each time and contains per-request values.

The newest 20 receipts are kept in the gt_performance_purge_receipts option. Tools > Verified purge receipts shows the latest 10, the health report summarizes the latest 5, and the gt-performance/list-purge-receipts ability pages through them.

Ordinary purges do not write receipts. The last Cloudflare purge (accepted, retrying or failed) is shown under Latest Cloudflare purge on the Cloudflare tab. See Purging.

Sources: PurgeVerifier, ResponseSnapshot.

Diagnostic logging

Optimization > Diagnostics > Diagnostic logging (the debug setting). It keeps the last 100 redacted plugin events in the database and adds cache-decision headers and a script-decision comment to public pages. Turn it on while investigating and off afterwards. While it is on:

  • plugin events are recorded (see below);
  • BYPASS, DYNAMIC and OPTIMIZE-ONLY responses carry X-GT-Cache and X-GT-Cache-Reason;
  • cache hits carry X-GT-Cache-Key;
  • when JavaScript minify, defer or delay is on, each optimized page ends with an HTML comment headed GT Performance JavaScript decisions that lists every printed script handle, what was done to it (defer, delay or keep) and why, for example jquery: keep - not deferred: jquery has inline code that runs right after it, plus a count of scripts WordPress did not register. View the page source to read it. See JavaScript Optimization.

Turning it on or off publishes the runtime config and, because the setting changes what pages carry, clears the page cache, so each page’s next response reflects the change.

Private diagnostics in the database

Log entries are stored in WordPress, in the non-autoloaded option gt_performance_diagnostic_log. No log file is written anywhere, including inside the cache directory.

  • Only the newest 100 entries are kept.
  • Each entry is time (UTC, ISO 8601), level, message and up to 20 context values.
  • Context values whose key looks like a secret (token, secret, password, key, cookie, nonce, authorization) are replaced with [redacted].
  • Messages and string values have URLs cut down to scheme, host and path (credentials and query strings removed), bearer tokens and password=/token=/api_key=-style pairs redacted, and are capped at 1,000 characters.
  • Nothing is recorded while Diagnostic logging is off, errors included.

There is no log viewer in the admin. Read it with WP-CLI:

wp option get gt_performance_diagnostic_log --format=json
[{"time":"2026-09-27T10:15:02+00:00","level":"debug","message":"Response not cached","context":{"reason":"status:404"}}]

Response not cached at debug level is normal: 404s, redirects and cookie-setting responses are supposed to be excluded. Look for error and warning entries such as Queue job failed, Cloudflare purge failed, xCloud cache purge failed, Unused CSS optimization failed; original HTML returned and Queue cron was missing and has been rescheduled.

Plaintext gt-performance.log files left in wp-content/cache/gt-performance/logs/ by older releases are deleted automatically. If they cannot be deleted, the admin shows “Old diagnostic files could not be removed” until the cache directory is writable again.

The option is removed on uninstall when Remove all data when the plugin is deleted is on.

Source: Logger.

Health report and Site Health

The health report is the site-wide view: queue backlog and age, the queue runner’s last run, WP-Cron, cache storage, drop-ins and the cache mode, edge ownership, runtime config publication, purge verification, unused CSS failures, active language and currency plugins, and cache warming. Its check names and messages are translatable. It reads saved state and cheap local checks only; it requests no pages and contacts no edge provider. It lives on Tools > Health, in wp gt-performance health, and as a redacted support download. See Tools.

The same checks run as a WordPress Site Health test, GT Performance health, under Tools > Site Health with a “Performance” badge:

Worst checkSite Health resultHeading
pass or infoGoodGT Performance is healthy
warningRecommendedGT Performance has items to review
failCriticalGT Performance cannot cache or publish correctly

Checks needing attention are listed in the test’s description, with a link to the GT Performance Tools tab.

wp gt-performance health and wp gt-performance doctor exit with status 1 when any check fails, so monitoring and CI can act on them. Warnings, such as an integration you do not use or an active language plugin, exit 0.

Sources: HealthReport, SiteHealth.

doctor

wp gt-performance doctor

A live environment check, printed as a table:

CheckPass when
PHP8.1 or later
WordPress6.6 or later
Cache directorywp-content/cache/gt-performance is writable
Page drop-inowned (otherwise missing or conflict)
WP_CACHEenabled
Redis drop-inowned (a warning when you do not use Redis is expected)
CloudflareEnabled (a warning when you do not use Cloudflare is expected)
xCloudNo conflict between xCloud and direct Cloudflare edge ownership
WP-CronSee below

The command exits with status 1 when any row is fail, and 0 when the worst is a warning.

Cron health

The background queue runs on WP-Cron every minute. When request spawning is enabled (the default), the WP-Cron check passes. With DISABLE_WP_CRON set, it warns when any scheduled event is more than 15 minutes overdue, and doctor prints a runner line for your server’s crontab:

*/5 * * * * flock -n /tmp/gt-performance-wp-cron.lock '/usr/bin/php' '/path/to/wp/wp-cron.php' >/dev/null 2>&1

(the PHP binary and path are filled in for your server). The health report and its support export say the same without the command, because the runner line carries an absolute server path; they point you to doctor for it. Running wp-cron.php covers every GT Performance event: the queue, garbage collection, Cloudflare purge retries and scheduled database cleanup. To drain only the queue by hand:

wp gt-performance queue run --limit=100

The health report also warns when the queue runner has not reported in for more than 15 minutes. See Tools.

Source: CronHealth.

Share

Was this helpful?

Still stuck? Open a support ticket