Troubleshooting
Start with the three read-only checks. Between them they answer most questions before you change anything:
wp gt-performance doctor
wp gt-performance health
wp gt-performance cache explain --page-url=https://example.com/the-page/
In the admin, the same evidence is on Tools (runtime status, Explain this page, health, purge receipts, queue) and CSS Status, and Setup > Verify checks a real cached response end to end. doctor and health exit with status 1 when a check fails, so they also work in monitoring scripts. See Diagnostics for what each output means.
Turn everything off without changing settings
Add this to wp-config.php:
define( 'GTPERF_SAFE_MODE', true );
While it is defined:
- no HTML transformation runs (unused CSS, JavaScript defer, delay and minified delivery, images, fonts, embeds, CDN rewriting);
- no unused CSS is generated;
- the page-cache drop-in serves nothing from the cache, so every request reaches WordPress;
- pages WordPress renders are not stored and are sent with
Cache-Control: no-store, private, max-age=0andX-GT-Cache: SAFE-MODE.
Settings, the drop-ins, WP_CACHE and the pages already on disk are left alone, and the admin keeps working, so you can compare the site with and without GT Performance’s changes and then remove the line. It is the answer to “something looks wrong and I cannot tell which option did it”. Safe mode covers the HTML pipeline and the page cache only; other settings, such as the WordPress quick toggles (emoji, Dashicons, feeds and so on), stay in effect. A copy held by Cloudflare or a host cache is outside its reach; purge it there if you need to see the uncached page through the edge.
Remove the line to return to normal, and the stored pages are served again.
To check a single page without touching wp-config.php, use Tools > Explain this page and follow Open this page with every optimization off. That link carries a gtperf_safe_mode nonce, works only for a signed-in administrator, and applies to that request alone. Source: SafeMode.
Nothing is being cached
Work down the list. Each step rules out the one above it.
- Run Setup’s Verify. Setup > Verify requests the home page as a visitor up to four times and says whether GT Performance served it from its cache, and if not, why. See Setup.
- Which mode is it in?
wp gt-performance cache statusprintsmode=storeormode=optimize-only. In optimize-only mode nothing is stored here by design: your host’s cache stores pages, andX-GT-CacheshowsOPTIMIZE-ONLY(with Diagnostic logging on) rather thanHIT. Check your host’s own cache header instead. See Page Cache. - Is the drop-in installed and
WP_CACHEon?doctorshould show Page drop-inownedand WP_CACHEenabled; the Dashboard’s Page cache card should say Active. In store mode nothing is stored until both are true.missing: install it from Setup (Save cache mode with Store pages), Dashboard > Operations > Install page-cache drop-in orwp gt-performance cache install-dropin.conflict: see Drop-in conflicts. - Is Enable origin page cache on? Page Cache tab. The health report shows “Origin page cache is disabled.” when it is off.
- Are you logged in?
wordpress_logged_in_is a bypass cookie. Test in a private window or withcurl. - Ask for the rule.
cache explain, or Tools > Explain this page, names the deciding rule inreason, for examplecookie:wordpress_logged_in_,path:/account/, orpage-optionfor a post with Don’t cache this page ticked in its editor box. - Look for an unknown query parameter. Any parameter not on the ignored list or the cache-separately list bypasses the cache by design (
unknown_query:…), and so does any parameter sent as an array (query_array:…). Add parameters that never change the page to Parameters that do not change content, and parameters whose every value is its own page to Cache each value separately, on the Exceptions tab. See Cache Exclusions. - Check the response. A page that sets a cookie, returns anything but 200, is not HTML, sends
privateorno-store, or definesDONOTCACHEPAGEis never stored. With Diagnostic logging on,X-GT-Cache: DYNAMICandX-GT-Cache-Reasontell you which. - Check the runtime config. The drop-in reads
wp-content/cache/gt-performance/config.json, encrypted with a key derived fromAUTH_KEY. It needs PHP OpenSSL and anAUTH_KEYof at least 16 characters that is not the sample phrase. If the file is missing or cannot be decrypted, the drop-in quietly hands every request to WordPress. The health report’s Runtime configuration check fails when the last save could not be published. - Did
AUTH_KEYchange? After rotating salts, both drop-ins cannot decrypt their config. Save GT Performance settings once to re-encrypt it with the new key.
Drop-in conflicts and ownership
GT Performance owns wp-content/advanced-cache.php and wp-content/object-cache.php only when they carry its signature. It never overwrites a drop-in another plugin owns.
| Status | Meaning | Fix |
|---|---|---|
owned | The GT Performance drop-in is in place | Nothing. It is refreshed automatically when the plugin version, the plugin’s location or the bundled drop-in changes |
missing | No drop-in | Install it from Setup, the Dashboard, or with wp gt-performance cache install-dropin. In optimize-only mode it is not needed |
conflict | Another plugin’s file is in place | Deactivate the other caching plugin (most remove their drop-in on deactivation). If the file is still there, delete it, then install GT Performance’s. If you want to keep the other cache, choose Optimize only on Setup instead |
The installer also stops, rather than guessing, when wp-config.php needs a hand edit:
- “wp-config.php contains a custom WP_CACHE declaration. Enable it manually before installing the drop-in.”
- “wp-config.php is not writable. Add define( ‘WP_CACHE’, true ); manually.”
Deactivating GT Performance removes both drop-ins when it owns them, restores its WP_CACHE change, empties its page store, removes its Cloudflare Cache Rule when Cloudflare is connected, and clears its scheduled events. See Installation and Redis Object Cache.
Source: DropinInstaller.
Cache directory is not writable
wp-content/cache/gt-performance must be writable by the web server user. doctor reports Cache directory fail, the health report’s Cache storage check fails, and Tools > Runtime status shows Not writable.
What you will see:
- No pages, generated CSS or font files can be stored. The site keeps working uncached.
- Settings saves are refused with “Settings could not be applied to the runtime cache. Previous settings were retained and may still be active. Check cache directory permissions and PHP OpenSSL, then save again.” The earlier settings stay in force.
- “Old diagnostic files could not be removed” when plaintext logs from an old release are still on disk.
Fix the ownership or permissions, then save settings once. If wp-content/cache or wp-content/cache/gt-performance is a symbolic link, GT Performance refuses to write through it; use a real directory.
A save can also be refused with “Another settings change is in progress. Your changes were not saved; try again in a moment.” when another save, import, restore or connection flow holds the settings lock. Wait a few seconds and save again. See Tools.
The site looks stale to me, but not to visitors
Expected while you are logged in: logged-in requests bypass the cache, so you always see fresh pages. If visitors see stale content, purge the URL and check Age and X-GT-Cache from a private window.
A page updated but visitors still see the old version
- See what an update to that post purges, and why:
wp gt-performance cache preview --post=123
Also check Cache clearing after publishing on the Page Cache tab. Do not clear automatically purges nothing. See Purging. - Purge the URL at the origin and the edge, then check the result:
wp gt-performance cache verify --page-url=https://example.com/the-page/
The receipt says whether the stored page was removed and whether the edge still served an old copy (first_was_old_hit). - Re-check with a throwaway query parameter, not the bare URL. The bare URL may be answered by an edge copy.
If a copy survives repeated purges with a growing Age, it is held by a layer GT Performance does not address: a host-managed Cloudflare, a host optimizer or a server cache in front of WordPress. Purge it from the host’s dashboard, or wait for its lifetime to run out. In optimize-only mode that layer is your host’s page cache by design. GT Performance purges Cloudflare and, through the xCloud integration, xCloud’s caches, but no other host’s cache, so purge it from the host after edits it does not clear on its own.
After a WordPress, plugin or theme update the whole cache is purged, so a page that still shows old assets after an update is coming from one of those layers too.
Cloudflare purges fail or do not take effect
- Read the result. The Cloudflare tab’s Latest Cloudflare purge shows Accepted by Cloudflare, Failed; retry scheduled or Failed; action needed, with the reason. When an interactive purge could not finish at the edge, the admin says “The local page cache was cleared, but Cloudflare could not finish its purge.”
- Temporary failures retry by themselves. Network errors, HTTP 429 and 5xx responses are retried up to three times through WP-Cron, 1, 2 and 4 minutes apart (or later when Cloudflare sends
Retry-After). Only the batches that failed are sent again. With WP-Cron not running, retries wait; see Diagnostics. - Walk the connection. Run connection check on the Cloudflare tab tests credentials, authentication, zone lookup and cache-rule read and write in order, and names the stage that fails. It also warns when the site’s DNS record is not proxied (grey cloud), in which case visitors never reach the Cloudflare cache, and when APO is on, whose copies GT Performance’s purges do not clear.
- Zone ID missing. “Cloudflare Zone ID is missing. Connect/sync Cloudflare before purging.” Run Connect/sync Cloudflare once.
- After updating the plugin, sync once. Connect/sync Cloudflare rewrites the managed cache rule to the current version and purges this site’s hostnames. Other rules in the zone are preserved.
- The rule disappeared. Deactivating GT Performance deletes its Cache Rule. After reactivating, the Cloudflare tab says so; run Connect/sync Cloudflare to restore it.
- Another site in the same zone lost its cache. A full purge now purges only this site’s hostnames, and each site keeps its own rule, so this no longer happens between sites on 1.2.0 or later.
- xCloud owns the edge. When the xCloud integration is on and xCloud reports its Cloudflare Enterprise add-on or its own edge cache, direct Cloudflare purges and rule sync are switched off so two owners never fight over one edge. The health report shows an Edge ownership warning when both claim it. With xCloud’s automatic cache purge plugin installed, direct Cloudflare is paused without a warning, and GT Performance purges xCloud Cloudflare Enterprise instead. See xCloud.
- The purge was accepted but the old copy stays. See the section above: another layer is holding it. Some host optimizers turn the plugin’s
no-store/privateresponses into cacheable ones, so a CDN in front of them keeps pages GT Performance never meant to be shared.
See Cloudflare.
Cart or account shows another visitor’s data
Stop and bypass the affected paths immediately under Exceptions > Cache exceptions > Paths that must stay dynamic, then purge:
wp gt-performance cache purge
wp gt-performance cache explain --page-url=https://example.com/cart/
cache explain must now report cacheable: false. Check that the matching adapter is on under Integrations > Commerce safeguards (FluentCart, Easy Digital Downloads, WooCommerce) and, with Cloudflare, that Cloudflare edge cache lifetime is 0 so the edge respects the origin’s no-store. Responses kept dynamic by a commerce or cache exception carry X-GT-Commerce-Cache: BYPASS.
Please report it on GitHub. This is the failure class the whole design exists to prevent. See Commerce Safety.
Layout breaks after enabling unused CSS
- Set Staged rollout (Optimization tab) to 0% – original CSS only. Original stylesheets come back at once, and the save clears cached pages.
- If the missing styles belong to something JavaScript adds after load (a table of contents, an ad, a slider), run Fetch classes now under Fetch important CSS classes on CSS Status. It opens two recent pages from each public post type in your browser and records what those scripts add, so builds keep their styles. It also runs by itself after unused CSS settings change.
- If the missing styles only appear after a click (an open menu, a modal), add a pattern to Selector safelist under Exceptions > Unused CSS exceptions. Plain text is a partial match;
/^\.modal(?:--|\b)/iis a regular expression. - If one stylesheet is the problem, add its URL (or inline style ID) to Excluded stylesheets.
- Keep Preserve dynamic states and Integrations > Automatic conflict protection on; the second keeps page-builder state classes for supported builders.
- Rebuild with Force regenerate URL or Force regenerate all CSS on CSS Status, check the page, then raise the rollout again.
See CSS Optimization.
Unused CSS builds fail or stay queued
CSS Status explains each page’s result in plain language; visitors keep the original CSS while a build is queued, building or failed, so nothing breaks. The common causes:
- “Needs cache setup”. In store mode, builds need the page-cache drop-in and
WP_CACHE, and the page must be cache-eligible. In optimize-only mode builds run without the drop-in, although this label still shows while it is absent. The build buttons are disabled while Remove unused CSS is off, Enable origin page cache is off, Staged rollout is 0% or safe mode is on. - Queued for a long time. Builds are background jobs, one per page, run by WP-Cron. Check Tools > Background queue: is optional work paused, are jobs failing? Check the health report’s queue, queue runner and WP-Cron rows.
wp gt-performance queue rundrains the queue by hand. - “The page was requested, but no build was recorded.” Each build is a loopback request: the server fetches its own page with a signed build token, a 30-second timeout and no redirects. The site must be able to reach itself. Firewalls, security plugins, HTTP authentication on a staging site and DNS that points the site’s own hostname elsewhere all break it.
- “Requesting the page returned HTTP …” Only pages that answer 200 can be built. A 404 or 410 result goes away on its own.
- “Reducing this page’s CSS took longer than 20 seconds.” Exclude that page’s largest stylesheet.
- Host or CDN caching the build request. Every build request carries a value no cache has seen (
gtperf_css_run), so a host optimizer or Cloudflare that stores the build response cannot answer the next build with it. If builds still come back without a report, make sure the loopback request reaches WordPress rather than a cached copy in front of it.
A failing build job is retried; after its third attempt it stays failed in the queue. Retry it from Tools > Background queue or wp gt-performance queue retry --id=<id> once the cause is fixed, or use Regenerate on the CSS Status row.
Bricks pages lose their base styles
Bricks 2 opens its framework CSS with a layer-order statement (@layer bricks.reset, …;). The pruner keeps leading layer statements apart and puts them back in front; a stylesheet with a layer statement after any rule is left untouched rather than risk changing layer order. Icon-font rules written as :before are tested like ::before.
Pages built by an older release keep the CSS they were built with until rebuilt. Run Force regenerate all CSS on CSS Status. While Bricks is active and Automatic conflict protection is on, its state classes (open menus, popups, sliders, animations) are kept as well.
Source: CssPruner.
Inline jQuery code breaks after enabling defer
Defer safe JavaScript follows WordPress’s script registry, the same rule core uses:
- a script stays blocking when inline code is attached right after it (
wp_add_inline_script()), or when a script depending on it cannot be deferred; - scripts WordPress did not register are never deferred, because their order is unknown.
Inline code added through WordPress’s script API is therefore safe. What the registry cannot see is a raw <script>jQuery(…)</script> a theme or plugin prints straight into the page. If jQuery is deferred and such code runs first, it fails. Options:
- Turn on Diagnostic logging and read the
GT Performance JavaScript decisionscomment at the end of the page source to see what was deferred and why. - Add the jQuery file to Never optimize scripts (Exceptions tab), for example
/wp-includes/js/jquery/jquery. Patterns match script URLs. - For one page, set JavaScript on this page to Do not defer or delay scripts in the GT Performance box in the editor.
Scripts whose URL contains cart, checkout or payment are never deferred or delayed. See JavaScript Optimization.
Source: ScriptPlan.
Something breaks after enabling JavaScript delay
Delay is the highest-risk switch. Turn it off first, confirm recovery, then turn it on again with fewer patterns in Scripts to delay. Do not debug delay and defer at the same time.
A script matching a delay pattern is delayed together with every registered script that depends on it, or not at all when any of them is excluded or has inline code attached. Consent banners, forms and checkout do not belong in the list. For one page, Do not delay scripts in the editor’s GT Performance box turns delay off.
Background jobs are stuck or failing
Open Tools > Background queue, or:
wp gt-performance queue status
wp gt-performance queue list --status=failed
- State: Paused. Only cache invalidation runs while optional work is paused. Warming, preloads, CSS builds, image variants, database cleanup and assistant operations wait. Resume queue or
wp gt-performance queue resume. - Nothing runs at all. The queue runs on WP-Cron every minute. Check the health report’s Queue runner and WP-Cron rows; see Diagnostics.
- Failed jobs. A job that throws is retried after 30 seconds, then 60; after the third attempt it is marked failed with its last error. A job whose worker crashes is reclaimed after its 10-minute lease and stops the same way after three attempts. Retry it once the cause is fixed.
- “Queue upgrade in progress”. After an update the jobs table is upgraded in small batches from admin screens and WP-CLI, never from visitor requests. Reload the Tools tab or repeat any
wp gt-performancecommand. If it persists, check database permissions and that the jobs table uses InnoDB.
See Tools.
Redis object cache will not install
Test and install Redis on the Object Cache tab tests the connection first and refuses rather than install a drop-in that cannot connect. The messages:
| Message | Fix |
|---|---|
| The PHP Redis extension is not installed. | Install PhpRedis on the server |
| Enable Redis object caching and save the settings first. | Turn it on and save before testing |
| Redis could not be reached with the saved credentials. | Check host, port, username, password and TLS, or the WP_REDIS_*/GTPERF_REDIS_* constants that override them |
| Redis accepted the connection but did not answer the health check. | Check the Redis server and ACL permissions |
| Another object-cache.php drop-in is already installed. | Remove the other plugin’s drop-in first |
See Redis Object Cache.
Constants in wp-config.php are being ignored
They must use the GTPERF_ prefix and be defined above the “That’s all, stop editing!” line. Old GTP_ constants are not read. For Redis, the WP_REDIS_* constants used by Redis Object Cache are also read. See Constants Reference.
Blank page or “critical error” after updating from 1.0.0
A site that still runs the drop-in generated by 1.0.0 or earlier loads a runtime file list that no longer matches the plugin. WP-CLI cannot help in this state because it boots WordPress and hits the same failure. Remove the drop-in at the filesystem level:
rm wp-content/advanced-cache.php
The site returns immediately (no drop-in simply means no page cache). Sign in to wp-admin and install the drop-in again from the Dashboard. The standalone repair script does this and the rest of the migration. See Upgrading.
The plugin will not activate on multisite
This is deliberate. Activation on a WordPress multisite network stops with: “GT Performance does not support WordPress multisite. Its cache configuration and cache directory are shared across a network, which would let one site’s settings decide another site’s cache behavior.” Use it on single-site installs only.