How WordPress Caching Works, Layer by Layer

Caching is the most powerful and least understood lever in WordPress performance. Most site owners install a caching plugin, see the green checkmark, and move on — without knowing that the plugin touched only one of six caching layers, that two of the others matter more, or that the layer causing their weird bug ("I updated the page and nothing changed!") is one they've never heard of.

This post walks the full stack in the order a request travels it: from the visitor's browser down to the database, explaining what each layer stores, what invalidates it, and the characteristic failure each one produces. By the end, "clear the cache" stops being a superstition and becomes a diagnosis.

The map: six layers between a visitor and your content

LayerLives whereStoresTypical lifetime
1. Browser cacheVisitor's deviceStatic files (CSS, JS, images, fonts)Hours to a year
2. CDN / edge cacheDatacenters near visitorsStatic files, sometimes full pagesMinutes to days
3. Full-page cacheYour server (or edge)Complete rendered HTML pagesMinutes to days
4. Object cacheServer memory (Redis/Memcached)Database query results, computed valuesSeconds to hours
5. OPcachePHP itselfCompiled PHP bytecodeUntil code changes
6. MySQL internalsDatabase serverBuffered data pages and indexesManaged automatically

Each layer exists to prevent work at the layers below it. A fully cached request is answered at layer 1 or 2 and your server never hears about it. A fully uncached request runs the gauntlet all the way to disk.

Layer 1: The browser cache — fast, free, and occasionally a liar

When your server sends a stylesheet, it attaches headers — Cache-Control, Expires, ETag — telling the browser how long it may reuse the file without asking again. Good hosts set static assets to cache for a long time (a year is common) because WordPress puts a version string on asset URLs (style.css?ver=6.5.1): change the file, change the URL, and the old cache entry is simply never requested again. This is called cache busting, and it's why long lifetimes are safe.

Characteristic failure: you (or your developer) edit CSS directly without bumping the version, and half your visitors see the old design for days. The fix is never "tell every visitor to hard-refresh"; it's versioned filenames or query strings so the URL itself changes.

What you control: mostly nothing per-visitor, which is the point. Set correct headers once — most performance plugins and all decent hosts do — and let it work.

Layer 2: The CDN — your site, photocopied around the world

A CDN keeps copies of your files in datacenters close to visitors, cutting the physical round-trip. For a typical WordPress site, the CDN caches static assets by default and passes HTML through to your server untouched. That distinction matters enormously: a default CDN setup does not cache your pages, only your images and scripts. Your server still renders every pageview.

The advanced move is edge full-page caching — telling the CDN to cache the HTML too. This is spectacular for anonymous traffic (your site survives traffic spikes that never reach your origin server at all) and genuinely tricky to get right, because the CDN must know never to cache pages for logged-in users, carts, or checkouts. That's done by bypassing cache when certain cookies are present. Get the cookie rules wrong and one user sees another user's account page — the single worst caching bug that exists.

Characteristic failure: stale HTML at the edge after you publish, because your purge integration isn't wired up. If you edit a post and the live site doesn't update but incognito-on-your-phone eventually does, suspect the edge.

Layer 3: The full-page cache — where the big numbers come from

This is the layer people mean when they say "caching plugin." The idea is blunt and effective: the first visitor to a URL makes WordPress do all its work — load PHP, run plugins, query the database, render the theme — and the resulting HTML gets stored. Every subsequent visitor gets that stored HTML directly. A page that took 800ms to generate is now served in a few milliseconds, and page caches implemented at the server level (nginx FastCGI cache, Varnish, or a host's built-in layer) don't even start PHP.

Three rules govern everything about page caching:

  1. It only applies to anonymous visitors. Logged-in users, commenters with cookies, and shoppers with carts get personalized pages, so the cache steps aside. A membership site where everyone is logged in gains almost nothing from this layer — an under-appreciated fact that explains many disappointing before/after tests.
  2. Invalidation is the whole game. When you publish a post, the cache must purge that post's page, the homepage, category archives, feeds — every page whose content changed. Good plugins and hosts handle these dependency chains automatically. Bad configurations either purge too little (stale content) or purge everything constantly (no cache benefit).
  3. Cache hit ratio is the metric that matters. A page cache that's technically enabled but hit only a small share of requests — because of short lifetimes, cache-busting query parameters from ad campaigns, or overly aggressive purging — delivers a fraction of its potential. If your host exposes hit-rate stats, watch them; a high hit rate for a content site is a reasonable expectation.

Characteristic failure: "I changed it and it still shows the old version" — the most common caching complaint in existence, and almost always this layer or layer 2. The diagnostic: check the page in a private window (rules out layer 1), then check response headers for cache HIT/MISS indicators most systems add.

Layer 4: The object cache — the one that helps when nothing else can

Here's where it gets interesting, because this layer helps exactly the requests the page cache abandons: logged-in users, admins, carts, API calls, search results.

WordPress internally caches the results of expensive operations — database queries, option lookups, user data — in an object cache. Out of the box, that cache lives in PHP memory and dies at the end of every request. Each new request rebuilds it from scratch, which is why stock WordPress hits the database with the same queries over and over.

A persistent object cache (Redis or Memcached, plus a drop-in plugin) keeps those results in shared memory between requests. The practical consequences:

  • wp-admin gets noticeably faster — often the most felt improvement of any caching change, because admins never benefit from page caching.
  • WooCommerce checkouts and carts, un-cacheable at the page level, get their query load slashed.
  • Transients (WordPress's temporary-data API) move out of the database into Redis, where expiry actually works properly.

Characteristic failure: stale data with confusing symptoms — a plugin setting you changed doesn't take effect, or a REST response shows old values — because some cached object wasn't invalidated. Rare with well-behaved plugins, maddening when it happens. wp cache flush is the reset button.

Layer 5: OPcache — the cache you should never think about (but should verify once)

PHP is a compiled-on-the-fly language: normally, every request would read your PHP files and compile them to bytecode before running anything. OPcache stores that compiled bytecode in memory, eliminating compile time entirely — worth a large constant speedup on every single request, cached or not, logged-in or not.

Every competent host enables OPcache. Your involvement should be limited to two checks:

  • Is it on and sized adequately? A site with many plugins can exceed a small OPcache memory allocation, causing constant eviction and recompilation. If your host exposes OPcache stats, look for a very high hit rate and no "out of memory" restarts.
  • Does it revalidate? OPcache checks whether files changed, on a timer. On rare aggressive configurations (revalidation off, common in some containerized setups), deployed code changes don't take effect until PHP restarts — the source of the truly baffling "I edited the file and nothing happened, and it's not the page cache" mystery.

Layer 6: MySQL's own caching — mostly not your job anymore

The database keeps its own hot data in memory: InnoDB's buffer pool holds recently used table and index pages, so repeated reads never touch disk. On managed hosting this is tuned for you and there's little to do. Two things worth knowing anyway: first, the old MySQL "query cache" that ancient tutorials tell you to enable was removed from MySQL 8 entirely because it hurt more than it helped — ignore any advice mentioning it. Second, if your database is genuinely too big for its buffer pool (think multi-gigabyte tables on a small VPS), no application-side caching fully hides that; the fix is data hygiene or better hardware.

Putting it together: a debugging flowchart for stale content

When someone says "the site is showing old content," walk the layers top-down:

  1. Private/incognito window shows correct content? It was the browser cache. Check that asset versioning is working.
  2. Incognito still stale, but the response headers show an edge cache HIT? It's the CDN. Purge, then fix the purge automation.
  3. Headers show your server's page cache serving it? Purge the page cache; if it recurs, your invalidation rules miss this content type.
  4. Dynamic data stale for logged-in users only? Object cache. Flush Redis and find the plugin that isn't invalidating.
  5. Code changes not taking effect at all? OPcache revalidation. Restart PHP.

Ninety-plus percent of stale-content mysteries die at steps 1–3.

What a sensible setup looks like

For a typical business site or blog: long-lived browser caching with versioned assets, a CDN for static files, server-level full-page caching with automatic purge-on-publish, Redis object caching, OPcache verified once and forgotten. For WooCommerce or membership sites, the same stack — but you should know your real performance ceiling is set by the object cache and PHP speed, not the page cache, because your most valuable visitors are logged in.

And one honest caveat to end on: caching multiplies the speed of a site that works; it doesn't fix one that doesn't. A 3-second uncached page is still a 3-second experience for every cache miss, every first visitor after a purge, every shopper at checkout. Cache aggressively — but when the uncached number is ugly, fix the underlying slowness too, because that's the number your customers meet at the worst possible moments.

Photo: Ivo Brasil on Pexels