You have cleared the cache. In the plugin, then the hosting panel, then a purge of everything at Cloudflare for good measure, and the page still shows the old headline. So you clear it again, harder, and nothing changes. (A purge is just the technical word for throwing away a stored copy so a fresh one gets made.)

This goes in circles because a WordPress page can be stored in seven places between the database and the visitor’s screen. Each keeps its own copy to save building the page again, and each has its own clear button in its own dashboard. Clearing six achieves nothing if the seventh holds the copy you are looking at, and they are indistinguishable from the front end. A stale page tells you nothing about which layer is serving it.

So stop clearing and start identifying. Three quick checks name the layer in about two minutes, and then you clear one thing on purpose. Nothing in the first few steps needs any code, and none of it can break the site. The last section is for people on 20i hosting only. Everything before it applies on any host.

Before you start

You need admin access to WordPress and your hosting control panel, plus the CDN dashboard if a CDN sits in front of the site. A CDN, or content delivery network, is a service such as Cloudflare that sits between visitors and your server, holding copies of your pages in data centres around the world. Somebody sets one up deliberately, so if that rings no bells you probably have not got one.

The deeper steps are quicker with SSH and WP-CLI, a command line on the server and WordPress’s own command line tool. Plenty of hosting has neither, so each step also has a route through the admin or the panel.

Clearing a cache is safe, costing a slower first load and nothing else. Two things deserve care. Editing .htaccess, where a typo returns a 500 error on every page, so save a copy first. And flushing a shared object cache, which can reach other sites on the server, so read Step 7 before running that one.

Step 1: Find the layer before you clear anything

Before touching anything, it helps to see how many places a copy of your page could be sitting. Switch to the second view for what each symptom usually points at.

Open the page in a private window. If it is correct there, it is your browser, and every server-side purge you have run was aimed at the wrong target.

Then load the page with an invented query string on the end, https://example.com/page/?cachetest=1. A query string is the bit after the question mark, and you can put anything there; it changes nothing. Most caches file their copies under the full URL, so a URL they have never seen has no stored copy and the page must be built from scratch. This is the most useful test in the article:

  • Query string correct, clean URL stale: something between the browser and PHP is storing the response. Work down the layers below.
  • Both stale: the page is being generated fresh and generating old content, so the problem is inside WordPress. Skip to the object cache and opcache steps.
  • Both correct, but the page you first looked at is not: you are on a different URL than you think, usually a trailing slash, a www difference, or an old permalink still resolving.

A few setups strip query strings before caching, so it is occasionally misleading, but right far more often than not.

Now ask the server for its headers, from outside your browser. Headers are the labels a server attaches to every page it sends, invisible on screen, and caching layers sign their work there. curl fetches a URL and prints them, and it ships with macOS and Windows. Open Terminal on a Mac, or Command Prompt on Windows, where you stop the command at the | because grep is not a Windows command:

# Headers only, run twice, so you can see whether `age` is climbing.
curl -sSI https://example.com/page/ | grep -iE 'cache|age|via|expires|x-'

If typing commands is not for you, your browser’s developer tools show the same thing: press F12, open the Network tab, reload the page, click the first entry and read the Response Headers panel.

Either way, age comes first. It counts the seconds since the stored copy was made, so anything above zero means you are being served a copy. Then each layer names itself. cf-cache-status is Cloudflare, where HIT means it is serving your pages from its own store and DYNAMIC means it is not storing them at all, however often you purge it. x-litespeed-cache is LiteSpeed, x-fastcgi-cache or x-nginx-cache is nginx caching at the host, x-varnish with a via header is Varnish (a caching layer some hosts run in front of WordPress), and x-cache is used by several CDNs and proxies.

Run the same command against the query string version. Clean URL HIT, query string MISS with correct content, and you have named the layer.

Step 2: The browser’s own cache

People skip this one, and it is the layer no button on any server can reach, because the copy is sitting on the visitor’s own computer. If a private window is correct, your browser has the page stored, and a hard reload clears it: hold Shift and click the reload button.

The version that matters is when it happens to visitors, and the cause is nearly always an over-enthusiastic performance tweak. Plenty of speed guides suggest .htaccess lines telling browsers to hold files for a long time, and it sweeps up your pages with the images. Look for this line, and copy the file before touching it:

# The line to hunt for in .htaccess. A long expiry on text/html means visitors
# keep yesterday's page and no server-side purge can reach them.
ExpiresByType text/html "access plus 1 month"

Set it to "access plus 0 seconds" or delete the line. Long expiry is right for images, CSS and JavaScript, because well-behaved themes put a version number in those file URLs and change it when the file changes, which makes browsers fetch the new one. Pages carry no version number, so browsers must be told to check back every time.

Step 3: A service worker

A service worker is a small piece of JavaScript a site installs into a visitor’s browser, where it sits between them and your server and answers requests from its own store. It is what makes a site work offline. If the site ever had a progressive web app or offline plugin, one may still be registered in visitors’ browsers, answering to nothing on your server. Removing the plugin does not remove workers already installed, which is what makes this one nasty. If none of that rings a bell you almost certainly have none, so skip to Step 4.

Check developer tools (F12), under Application, then Service Workers. Unregistering there fixes your own machine. To reach everyone else, serve a replacement worker at exactly the same path as the old one. Browsers check for an updated worker, so one that tidies up and removes itself runs once and is then gone:

// Replaces the old service worker at the same URL, then deletes itself.
self.addEventListener('install', () => self.skipWaiting());

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches
      .keys()
      .then((keys) => Promise.all(keys.map((key) => caches.delete(key))))
      .then(() => self.registration.unregister())
      .then(() => self.clients.matchAll())
      // Reload open tabs so the visitor gets the real site immediately.
      .then((clients) => clients.forEach((client) => client.navigate(client.url)))
  );
});

Leave that file in place for a long time, since it only runs when a returning visitor comes back. Deleting the old file instead does not reliably unregister anything.

Step 4: The CDN or proxy in front of the site

Cloudflare is the common case, and the first question is whether it stores your pages at all. By default it stores images, CSS and JavaScript but not pages, and cf-cache-status: DYNAMIC confirms that. If it reads HIT, something turned page caching on: a cache rule, a page rule set to cache everything, or automatic platform optimisation.

Purge from the dashboard, under Caching, either everything or by URL. Purging by URL is worth learning, because “purge everything” on a large site throws away thousands of copies that were perfectly good. Development Mode, in the same section, is the diagnostic: it passes everything straight through to your server for a few hours, so if the page is right with it on and stale with it off, Cloudflare is holding the old copy.

One trap. Purging the CDN while the host’s cache still holds the stale copy just pulls that copy back out again. Clear from the inside out: your server first, the CDN last. In caching jargon your server is the origin and the CDN’s outer layer is the edge, so the rule is origin first, edge last.

Step 5: The host’s server-level cache

This layer is forgotten most often, because it sits outside WordPress and outside your CDN dashboard and nothing in the admin mentions it. Varnish, nginx FastCGI caching, LiteSpeed and 20i’s StackCache all live here.

On managed and shared hosting there is a button: find the caching section of your hosting control panel and clear it there. If you cannot find one, ask support to clear the server cache for your site, a request that makes perfect sense to them. With shell access, Varnish is cleared by banning URLs, varnishadm "ban req.url ~ ." clearing everything, while nginx FastCGI caching keeps files under the directory named by fastcgi_cache_path and clearing means deleting them.

The point to hold on to is that this layer is invisible to WordPress. A caching plugin’s “clear cache” button clears its own storage, and reaches the host’s cache only if the host wrote an integration.

Step 6: The WordPress caching plugin

By now this is often not the culprit, but clear it properly anyway. Use the plugin’s own “clear all” rather than a partial purge, and if it offers preloading, clear first and preload second, since preloading before purging refills the cache with the same stale pages.

Most cache plugins also ship a WP-CLI command, which is quicker if you have SSH. The button in the plugin does exactly the same thing, so skip this if you do not:

# The exact command varies by plugin, so check its documentation.
wp litespeed-purge all
wp w3-total-cache flush all

A frequent cause of “the plugin will not clear” is two page caching plugins at once, or a page cache plugin on a host that already runs a server-level cache. They fight, and clearing one leaves the other serving. Pick one and disable the rest.

Step 7: The object cache

If both the clean URL and the query string version are stale, WordPress is building the page fresh and building it wrong. The object cache is the usual reason. Every other layer stores finished pages; this one stores the ingredients. Database results and site settings are held in memory so WordPress need not ask again, and one out-of-date ingredient gives you a page built correctly from wrong data. Redis and Memcached are the two programs that normally do this, and you only have one if your host or a plugin set it up.

With SSH and WP-CLI, this clears it:

wp cache flush

One warning, which is why to read before typing. Where several sites share one Redis or Memcached installation without separate key prefixes, wp cache flush can clear entries belonging to those other sites. Nothing is lost permanently, but their sites get slow for a while. On shared hosting, prefer the flush control in your hosting panel, which is usually scoped to your site.

Without an object cache, WordPress does something similar on a smaller scale using transients. A transient is a value a plugin parks in the database with a use-by date on it, so a plugin holding a query result in one for twelve hours shows old data for twelve hours whatever you purge. Most cache plugins have a button to clear them, and wp transient delete --all does it safely over SSH. If you are chasing this repeatedly on a busy site, the real issue is how much work the site does per request, which is speed optimisation rather than caching.

Step 8: Opcache

This only affects you if you edit code, so skip it if you do not. Opcache has a distinct signature: you edited a PHP file, uploaded it, and nothing changed, with no page cache involved. Opcache is the part of PHP that keeps your code in memory in a pre-chewed form so it need not be read and interpreted on every request. Told not to check whether the files on disk have changed, it happily keeps running last week’s code.

The clean fix is restarting PHP, which most hosting panels offer as a button, sometimes labelled restart PHP-FPM or flush opcache. Where no such control exists, opcache_reset() run through the web server clears it for the website. Running it from the command line will not help, because the command line has a separate opcache the website never uses.

On a server you control, opcache.validate_timestamps on with a short opcache.revalidate_freq is the sane setting during development. Turning validation off assumes a deployment that restarts PHP.

For 20i hosting only: when the purge fired but covered one URL

Skip this unless the site is on 20i, or with a reseller who uses 20i, and StackCache is enabled. It applies to no other host.

Everything appears to work and the site is still stale: the purge fired, the headers show a fresh copy, the post’s own URL is correct, and the homepage and archives still list the old version. That is not a failed purge, it is one that was too narrow. StackCache clears the edited post’s URL and nothing else, so every page that merely lists it keeps its stored copy until it expires.

The manual answer is to clear StackCache entirely from the 20i control panel after publishing, rather than relying on the automatic purge. If you publish rarely, that is all you need.

I got tired of doing it, so I wrote a small free plugin: ABCode Improved Caching for 20i StackCache, listed on wordpress.org like the rest of my free plugins. It widens the purge.

  • Purges the homepage, the term archives, the post type archive and the main feed whenever a post changes, not only the post’s own URL.
  • Runs those purges in a background task, so saving stays fast.
  • Adds “Purge This Page” to the admin bar on front end views and “Purge Everything” anywhere in the admin, both protected by a nonce, the one-time token confirming the request came from you.
  • Hands the purged pages to ABCode Cache Warmer, if you have it, to be rebuilt immediately.

It is not a caching plugin and will not speed up an uncached site, it knows nothing about Cloudflare or any other CDN, and without StackCache underneath it it does nothing at all, on purpose, and says so in the admin rather than sitting quiet and looking functional. On 20i with StackCache running, it removes this problem from your day: the purge covers the pages that list the post, not just the post itself.

How to check it worked

Change something unmistakable, a heading rather than a comma, then fetch the page with curl rather than a browser, so you are reading the server’s answer and not your own machine’s memory of it. The new text should be there and age should be at or near zero.

Then try a private window, then your normal browser. All three agreeing means the change reached every layer. Curl right and private window wrong is a CDN location catching up, and it will. Both right and only your normal browser wrong is your own browser, so no visitor is affected and you can stop worrying.

When it does not work

It clears, then goes stale again within seconds

Something is refilling the cache from a stale source, the classic result of purging in the wrong order: the CDN was cleared, went to the origin, and was handed the old page by a server cache nobody touched.

Only the homepage is stale, everything else is fine

Not a failed purge, but a purge that only covered the URL you edited. Listing pages need clearing explicitly, which is true in principle of every cache.

The admin area itself is stale

An admin page should never be cached. Either a cache rule is matching /wp-admin/, or a proxy is ignoring the no-cache headers WordPress sends. Check the CDN rules first.

Nothing at any layer explains it

Check the source instead. Look at the post in the admin, and check whether a page builder is rendering from its own stored copy, since Elementor for one keeps generated CSS files that regenerate separately. If the content is wrong at source, no cache was involved and you may have a site that needs repairing.

Common questions

How do I tell which cache is serving the page?

Response headers, every time. Run curl -sSI on the URL and look for age, cf-cache-status, x-litespeed-cache and x-varnish. Each layer names itself, and the name tells you which button to press.

Why does clearing everything not work?

Because “everything” means everything in one dashboard, and the layer holding the page is in a different one. A layer can also be cleared correctly and then repopulate from a stale layer behind it.

Why does my site cache when I never installed a caching plugin?

Because your host almost certainly caches at the server level whether or not you asked. That is normal, and it is the biggest single reason people cannot find the cache they are trying to clear.

Working the layers instead of the buttons

Most of the frustration here comes from treating caching as one thing with one button. It is a stack, every layer is doing what it was told, and the page is not stuck but stored. The only question worth asking is where.

The habit to build is short: private window, query string, headers. Two minutes of that names the layer, and then you clear one thing deliberately instead of six things hopefully. Order matters: origin first, edge last, or you will cache the stale copy again higher up. If you have inherited a site with four caching layers and no documentation of any of them, drop me a message and I will untangle it.