How WordPress caching actually works

WordPress caching is not one thing. It is at least four separate layers, each solving a different problem, each with its own failure modes, and each configured in a different place. This article explains what each layer actually does, which layer a caching plugin is talking about when it says caching, and which problems caching does not solve at all.

When someone says "I installed a caching plugin", they are almost always talking about page caching. When someone says "my host enabled Redis for me", they are talking about object caching. When someone says "clear your browser cache to fix the slow site", they are talking about a third thing that does not affect server performance at all. These are not alternatives. They are layers, each solving a different problem, and a well-tuned WordPress site uses several of them at once. This article is about the model: what each layer caches, when it helps, when it gets in the way, and which problems caching cannot solve no matter how you stack the layers.

The three main layers on a WordPress site

A production WordPress response typically passes through three cache layers before a visitor sees it, plus a fourth that lives in front of everything else (the CDN). Each layer caches a different kind of thing, at a different stage, with a different lifetime.

Layer What is cached Who serves it Survives PHP crash Per-user
Page cache (full-page) Complete HTML response Web server or PHP plugin Yes No (bypassed for logged-in)
Object cache (persistent) Database query results, computed values Redis or Memcached Yes No
Object cache (non-persistent) Database query results within one request PHP itself, in-memory No (per request) No
Browser cache Static assets (CSS, JS, images, fonts) The visitor's browser Yes Yes (per device)
CDN edge cache Static assets and sometimes HTML Edge server near the visitor Yes No

The rule that makes sense of all of this: each layer caches something closer to the output than the one below it, and each layer skips more work than the one below it. A page cache skips the most work (no PHP runs at all). A browser cache skips the least (the browser just avoids redownloading a file it already has). Everything else sits between those two.

The WordPress advanced administration handbook on caching frames it the same way: "moving data from a place of expensive and slow retrieval to a place of cheap and fast retrieval." The layers exist because there are several different "slow places" in a WordPress page build, and you cannot reach them all from the same spot.

What page caching actually does

Page caching stores the complete HTML response for a URL and returns that HTML for subsequent visitors without touching PHP or the database. On an uncached request, WordPress boots, every active plugin loads, dozens of database queries run, and the theme assembles the HTML. On a cached hit, none of that happens. The web server reads a pre-built HTML file and sends it.

This is the layer that delivers the dramatic "my site is ten times faster" numbers. The WordPress handbook describes it as "reducing the processing load on the server" and notes that page caching "can improve performance several hundredfold for relatively static content" (source). The reason those numbers are real is that PHP execution and database queries together make up the bulk of a WordPress page's TTFB, and page caching skips both.

Page caching exists in two shapes:

  • Plugin-driven page caching. A plugin like WP Rocket, WP Super Cache, W3 Total Cache, or LiteSpeed Cache creates a wp-content/advanced-cache.php drop-in file, which WordPress loads early in the request if define('WP_CACHE', true) is set in wp-config.php. The plugin writes static HTML files to disk (or memory) and serves them on subsequent hits. PHP still starts up but exits early.
  • Server-level page caching. nginx fastcgi_cache, Varnish, or LiteSpeed LSCache serve cached HTML from the web server itself, before PHP is even invoked. A typical nginx fastcgi_cache configuration stores pages on disk with a keys_zone directive and serves them until a bypass condition is met. This is faster than plugin caching because PHP is never started at all, and it is the approach managed WordPress hosts generally use.

When page caching is bypassed (if caching is not working as expected, see WordPress cache not working for the troubleshooting path)

Page caching only works when the same HTML is correct for every visitor. The moment a page has to be different per visitor, the cache has to step out of the way. Every serious page cache bypasses itself for:

  • Logged-in users. The admin bar, personalized greetings, and draft previews all depend on who is looking. Serving a cached copy to a logged-in editor would show them the logged-out version, which is why the WordPress admin feels slow even on sites where the public front end is fast.
  • POST requests. Form submissions, comments, and WooCommerce checkout steps must always hit live PHP.
  • URLs with query strings that affect the response. Search results, filtered product archives.
  • WooCommerce cart and checkout pages. WooCommerce's own documentation is explicit: cart, my account, and checkout "need to stay dynamic since they display information specific to the current customer and their cart." The cookies that must trigger bypass include woocommerce_cart_hash, woocommerce_items_in_cart, and wp_woocommerce_session_. This is the single most important reason page caching does not fix a slow WooCommerce checkout: the checkout is, by design, never cached.
  • Feeds, sitemaps, wp-admin, xmlrpc. These have their own rules.

A well-written cache plugin handles all of this automatically. A poorly-configured one caches the cart page and serves stranger A's shopping cart to stranger B. I have seen exactly that happen on a WooCommerce site where someone installed two page cache plugins and the second one did not know about the first one's bypass rules.

Object caching: transients vs. the persistent object cache

Page caching helps when the whole HTML can be reused. Object caching helps when only parts of the page can be reused, or when two different pages share the same expensive query result.

WordPress has had an object cache since version 2.0. By default it is non-persistent: it lives in PHP memory for the duration of a single request and is thrown away when the request ends. That still helps, because a typical page build calls get_option() and get_post_meta() many times, and the second call within the same request is served from memory. The WP_Object_Cache class reference explains it clearly: "Cached data will not be stored persistently across page loads unless you install a persistent caching plugin."

A persistent object cache is what most people mean when they say "install Redis for WordPress" or "enable Memcached". A drop-in file at wp-content/object-cache.php replaces WordPress's default object cache with a backend (Redis or Memcached) that survives across requests. Query results cached on one request can be reused on the next. The WordPress handbook describes object caching as "moving data from a place of expensive and slow retrieval to a place of cheap and fast retrieval", and that is exactly what a persistent backend does: it moves data that would otherwise have to be fetched from MySQL into Redis, where it can be fetched in under a millisecond.

The transients API and why "transients are not in the database" matters

The Transients API is WordPress's built-in way to store a value with an expiry. On a site with no persistent object cache, transients are stored in the wp_options table with a _transient_ prefix and a companion _transient_timeout_ key. On a site with a persistent object cache, transients are stored in the object cache backend instead, and wp_options is bypassed entirely.

This is why the Transients API documentation warns: "Transients should also never be assumed to be in the database, since they may not be stored there at all." It is also why another warning matters: "Transient expiration times are a maximum time. There is no minimum age." If the object cache runs out of memory, it can evict a transient before its TTL, so any code that uses transients must be written to regenerate the value if it is missing. A plugin that assumes its transient is always there will produce stale or empty results the first time Redis hits its maxmemory ceiling.

What the recent WordPress releases actually changed

The object cache API has evolved in a way that is worth pinning to specific versions. The cache API version table on the WP_Object_Cache reference page lists the precise additions:

  • WordPress 6.0 (May 2022) added batch cache functions: wp_cache_add_multiple(), wp_cache_set_multiple(), wp_cache_delete_multiple(), and wp_cache_flush_runtime(). These let plugins and core fetch or set many keys in a single round trip to the backend instead of one per key (official dev notes).
  • WordPress 6.1 (November 2022) added wp_cache_supports() and wp_cache_flush_group() (function reference), giving cache implementations a formal capability-declaration API. 6.1 also added two Site Health checks: Persistent Object Cache (recommends Redis or Memcached when a site's scale warrants it) and Full Page Cache (detects full-page caching and evaluates response time against a 600 ms threshold). The Make WordPress post announcing these checks is the primary source. Note that 6.1 did not change how the object cache internally works; it gave third-party object cache plugins a standardized way to advertise their capabilities and gave administrators visibility into whether a persistent cache was actually installed.
  • WordPress 6.3 (August 2023) was the release with the bigger internal changes. It introduced dedicated query cache groups (post-queries, term-queries, comment-queries) and wp_cache_set_last_changed(), so that WP_Query results, term queries, and comment queries could be cached in named groups and flushed in bulk when underlying data changed. The 6.3 dev notes describe the reasoning in detail.
  • WordPress 6.4 (November 2023) reordered the get_option() cache lookup to check the object cache before the database, made WP_Query force split queries when a persistent object cache is active, and added a cache_results parameter to WP_Term_Query (dev notes). The net effect is that more of WordPress's internal work actually uses the object cache, if one is installed, on versions 6.4 and up.

The upshot for a site owner: if you are running WordPress 6.4 or newer and you have a persistent object cache installed, the object cache is doing more work for you than it was doing on 6.0. If you are running 6.4 without a persistent object cache, none of these improvements help you, because there is no persistent backend for them to use.

Browser caching and the Cache-Control header

Browser caching is the layer that most people misunderstand the most. It is not managed by a plugin. It is not something WordPress does. It is a contract between the web server and the visitor's browser, negotiated through HTTP response headers, that says "you already have a copy of this file, reuse it instead of downloading it again."

The headers that matter are all documented in web.dev's HTTP cache article:

  • Cache-Control: max-age=31536000 tells the browser to reuse the file for up to one year (31,536,000 seconds is the maximum practical value). This is appropriate for versioned or hashed static assets: a file named main.a3f2b1.css cannot meaningfully change, because any change produces a new filename.
  • Cache-Control: no-cache does not mean "never cache". It means "check with the server before using the cached copy" (revalidation). This is appropriate for HTML responses, so that a browser holding yesterday's HTML does not serve it when today's HTML exists.
  • Cache-Control: no-store means the browser and any intermediate caches (including CDNs) must never store a copy at all. Appropriate for bank statements and session-specific personal data.
  • immutable signals that the file will never change; the browser can skip revalidation entirely. As web.dev notes, "immutable will be ignored in some browsers", so it is an optimization, not a guarantee.

The important thing about browser caching is what it cannot do for a WordPress site. It only affects repeat visits by the same visitor. A first-time visitor has an empty browser cache and downloads everything. Browser caching also only applies to files the server has already sent, which means it does nothing for TTFB, PHP execution time, database query time, or server-side page cache hits. This is why the common support advice "clear your browser cache to fix the slow site" is misguided: clearing the browser cache forces a redownload of assets, which is the opposite of faster. Browser caching reduces repeat-visit load times by making the second visit cheaper. It has no effect on the first visit and no effect on the server.

CDN edge caching: where it differs from server-side caching

A CDN (Cloudflare, Fastly, Bunny, KeyCDN, and similar) puts a cache server in each of dozens of geographic locations, so that a visitor in Sydney fetches from a Sydney edge instead of from an origin in Amsterdam. Edge caching is a distinct thing from the page cache on the origin:

  • What a CDN always caches. Static assets (CSS, JS, images, fonts) that are served with a long Cache-Control: max-age. This is the easy win and the reason every serious WordPress site should have a CDN for static assets.
  • What a CDN sometimes caches. Full HTML pages, if explicitly configured to do so (Cloudflare's "cache everything" page rule, Fastly's custom VCL, Bunny's Perma-Cache). This is equivalent to moving the full-page cache from the origin to the edge. It requires the same bypass rules as origin-level page caching: cart, checkout, logged-in users.
  • What a CDN almost never caches. Dynamic HTML for logged-in users, REST API responses, admin requests. These bypass the edge cache and travel the full distance to the origin.

The misreading: "I added a CDN and my WooCommerce checkout is still slow." A CDN makes static assets and cacheable HTML faster for visitors far from your origin. It does nothing for a slow origin on dynamic pages, because the CDN has to forward those requests to the origin anyway. If your site is slow because PHP and the database are slow, adding a CDN moves the problem closer to the visitor but does not remove it. See the woocommerce-is-slow article for why the checkout path is particularly resistant to any kind of caching.

Choosing a caching plugin (and what the plugin is actually doing)

When someone asks "which caching plugin should I use?", the honest first question is: "which layer do you need?" Most of the major plugins are competing on the page cache layer (and, to a lesser extent, browser-header management and CDN integration). They do not replace a persistent object cache; they coexist with it.

The reason multiple page cache plugins conflict is documented on the WordPress performance tracker and in a widely cited LiteSpeed blog post: the wp-content/advanced-cache.php file is a drop-in, and only one plugin can own it at a time. If two page cache plugins both try to write to it, the second one overwrites the first, both think they are in charge, and cache invalidation breaks in hard-to-diagnose ways. The general rule is: exactly one page cache. You can safely combine one page cache plugin with a separate object cache plugin (Redis Object Cache or an equivalent) because they target different drop-ins (advanced-cache.php vs. object-cache.php).

What caching is NOT

Most confusion about WordPress caching comes from treating it as a single thing that either works or does not. It is not. These are the specific things caching is not.

  • Not one thing. "Enable caching" is four different answers depending on which layer you mean. A caching plugin typically handles page caching and may manage browser-cache headers. It does not by itself give you object caching unless you install Redis or Memcached separately. It does not give you CDN caching unless you sign up for a CDN.
  • Not a fix for a slow WooCommerce checkout. The checkout is, by design and by WooCommerce's own documentation, never served from page cache. Object caching does speed up the database queries behind the product pages and the product catalog, but it does not cache the checkout response itself. If your checkout is slow, caching is the wrong layer to look at. Focus on PHP execution, database indexes, and the payment-gateway API calls in the checkout path.
  • Not fixed by clearing the browser cache. The browser cache is the layer farthest from the server and has no effect on how long the server takes to respond. Clearing it forces a redownload, which is the opposite of faster. The only thing clearing the browser cache fixes is a stale asset that was already downloaded by this specific browser.
  • Not "more is always better". Two page cache plugins is not twice as much caching. It is a race condition over advanced-cache.php. Use exactly one plugin per layer. Object caching (Redis) and page caching (WP Rocket, say) can coexist because they target different drop-ins. Two plugins of the same layer cannot.
  • Not a substitute for fixing the underlying code. A plugin that does a synchronous remote API call on every page build will still hit the remote API on every cache miss. Caching helps for the cached hits; it does not help for anything that bypasses the cache, and on a WooCommerce site a significant portion of the request volume does bypass the cache. Caching buys you headroom. It does not replace writing efficient code.
  • Not free. A persistent object cache consumes memory (Redis keeps everything in RAM by default). A page cache consumes disk space. A CDN costs money. And every cache has an invalidation problem: when the underlying data changes, the cache has to know. A site that caches too aggressively serves stale prices, stale stock, or stale content. Phil Karlton's famous quote about the two hard problems in computer science includes cache invalidation for a reason.

Where to go next

If you arrived here because your server is slow even on simple pages, the high TTFB article covers what that metric actually includes and which cache layer helps for which part of it. If the whole site feels slow and you are trying to narrow down which layer is the bottleneck, the "why a WordPress site feels slow" article walks through how to break a request into its network, server, and browser phases. If the symptom is specifically that WooCommerce checkout or cart is slow, the "WooCommerce is slow" article explains why the dynamic pages are uncacheable and where to look instead. And if you are curious about the one cache layer that sits below all four of the above, PHP OPcache configuration for WordPress covers the bytecode cache that helps every uncached request, regardless of which of these four layers is involved.

Done chasing slowdowns?

Performance issues tend to come back after quick fixes. WordPress maintenance keeps updates, caching and limits consistent.

Have your WordPress site maintained

Search this site

Start typing to search, or browse the knowledge base and blog.