WordPress caching works in layers: a page cache for anonymous visitors, a persistent object cache such as Redis, OPcache for PHP itself, and browser and CDN caches for static files. Many slow sites have caching switched on and still load slowly, because a layer is misconfigured, purges too often or caches the wrong pages.
This guide goes through each layer, where it fits in a typical stack, and the mistakes that catch real projects.
What this covers
- The difference between page caching, object caching, browser caching, and CDN edge caching
- When to use Redis object caching versus the Transients API
- Why OPcache matters even when you “have caching”
- How to avoid caching personalised content, WooCommerce carts, and logged-in pages
- A layered set-up for brochure sites, content sites and WooCommerce
Caching layers at a glance
A healthy WordPress setup usually uses more than one cache:
- Browser cache for static files (images, CSS, JS)
- CDN edge cache to serve those assets close to users
- Full page cache (static HTML) for anonymous traffic
- PHP OPcache to avoid re-compiling PHP on every request
- Persistent object cache (Redis or Memcached) for repeated database work
- Application caching using transients or the object cache API for expensive computations
Aim for pages that are fast, correct and predictable, rather than for as much caching as possible.
1. Page caching (full HTML)
Page caching stores a rendered HTML response and serves it back without running WordPress at all. It usually does more for time to first byte than any other layer.
Best for: anonymous traffic, content pages, blog archives, marketing pages.
Be careful with: logged-in users, personalisation, dynamic pricing, cart and checkout.
Common options
- FlyingPress: good defaults and cache invalidation
- WP Rocket: widely used, and compatible with most themes and plugins
- Cache Enabler: lightweight, with fewer options
- Server-level FastCGI cache (NGINX): handles heavy traffic with little PHP load
NGINX FastCGI cache example
For server-level page caching, NGINX stores the responses PHP-FPM returns, rather than writing .html files.
# fastcgi_cache_path and map belong in the http block, usually nginx.conf
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
map $http_cookie $skip_cache {
default 0;
~*wordpress_logged_in 1;
~*comment_author 1;
}
server {
set $no_cache 0;
if ($request_method = POST) { set $no_cache 1; }
if ($query_string != "") { set $no_cache 1; }
if ($skip_cache = 1) { set $no_cache 1; }
if ($request_uri ~* "/(wp-admin/|wp-login.php|cart|checkout|my-account)") { set $no_cache 1; }
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_cache WORDPRESS;
fastcgi_cache_bypass $no_cache;
fastcgi_no_cache $no_cache;
fastcgi_cache_valid 200 301 302 10m;
add_header X-Cache $upstream_cache_status;
}
}
If you ever cache everything at the edge as well, you need the same skip-when-logged-in rule there.
Purge rules
A cache that never purges serves stale content in the end, and one that purges constantly saves little work.
At minimum, purge on:
- post/page publish and update
- taxonomy term changes that affect archives
- navigation menu updates
- theme or plugin updates that affect templates
- WooCommerce product changes (plus special handling for stock and pricing)
Most caching plugins handle this. If you build your own, use save_post, the menu update hooks and WooCommerce’s hooks.
2. Persistent object caching (Redis or Memcached)
Object caching stores the results of expensive operations (often database query results) in memory so WordPress can reuse them across requests.
Without a persistent object cache, WordPress still has an “object cache”, but it only lasts for a single request. Busy sites need one that persists between requests.
Best for: sites with lots of repeated queries, WooCommerce filters, ACF-heavy pages, multisite, high traffic.
Less important for: tiny brochure sites where full page caching already serves most visits.
Setting it up
- Redis is the common choice for WordPress hosting and is well supported.
- Use the Redis Object Cache plugin (or your host’s built-in version).
- Check that the cache persists, on the plugin’s admin page or in the response headers.
Using the object cache API in custom code
Use groups, sensible expiry times and deliberate invalidation, rather than caching forever.
$cache_key = 'top_products_v1';
$group = 'catalog';
$data = wp_cache_get( $cache_key, $group );
if ( false === $data ) {
$data = expensive_query_or_computation();
wp_cache_set( $cache_key, $data, $group, HOUR_IN_SECONDS );
}
Tip: treat cache keys as versioned. If your logic changes, bump the suffix.
3. Transients API
Transients are WordPress’ built-in time-based caching for key-value data. They are stored in the database (usually wp_options) unless a persistent object cache is available, in which case they can be stored there instead.
Best for: third-party API responses, expensive computed summaries, “top posts” lists, and anything that can be slightly stale.
$data = get_transient( 'top_posts' );
if ( false === $data ) {
$data = build_top_posts();
set_transient( 'top_posts', $data, HOUR_IN_SECONDS );
}
When transients become a problem
On busy sites without Redis, a careless transient strategy can bloat wp_options and increase overhead. A transient set without an expiry is autoloaded, so WordPress loads it on every request whether it is needed or not. Always pass an expiry.
In short:
- use transients for computed results
- don’t store huge objects in them
- clear them promptly when correctness matters, as with pricing, stock and membership
4. OPcache (PHP opcode caching)
OPcache caches compiled PHP bytecode, avoiding repeated parsing and compilation. It is a different layer to page caching and object caching, and it still helps even when you use both.
Many hosts enable OPcache by default, but don’t assume. Confirm it in your host panel or via phpinfo() in a safe environment.
Why it matters:
- WordPress loads a lot of PHP files on uncached requests
- admin and logged-in pages often bypass full page caching
- plugin-heavy sites benefit more
5. CDN and edge caching
A CDN serves files from servers near each visitor, and keeps much of the traffic off your own server.
Cache static assets at the edge:
- images
- CSS and JS
- fonts (with correct CORS headers)
Be cautious with HTML at the edge. Cloudflare’s Cache Everything works well for content pages, as long as it’s paired with bypass rules for logged-in sessions and dynamic endpoints.
Cloudflare rules to set
- Bypass cache for
/wp-admin/*,/wp-login.php - Bypass cache when cookie contains
wordpress_logged_in - Bypass cache for
cart,checkout,my-account - Cache static assets with a long Edge TTL
- Cache HTML only for safe URL patterns (for example
/blog/*)
6. Browser caching and headers
It’s the simplest layer, and it still gets missed.
- Set far-future cache headers on versioned assets (the filenames change when content changes)
- Use
Cache-Control: public, max-age=...on assets - Fingerprint asset filenames in your build, or at least add a version query string
Most performance plugins can set these. Check the result in DevTools and with Lighthouse.
7. Invalidation, and what never to cache
A cache that serves one visitor’s page to another, even once, damages trust. The risk rises with:
- WooCommerce
- membership content
- personalisation (greetings, pricing, location-based content)
- forms, previews, search, and logged-in toolbars
Keep a list of the endpoints that must never be cached:
wp-admin/*,wp-login.php/?preview=trueand post preview URLswp-json/*(usually)/cart,/checkout,/my-accountand AJAX endpoints- any “quote builder”, “dashboard”, “portal”, or account area
Then enforce it at every layer: plugin, NGINX, and CDN.
8. Layered set-ups by type of site
Content-led site (blog, brochure, marketing)
- Full page cache for anonymous traffic
- CDN for static assets (and optionally HTML on safe paths)
- OPcache enabled
- Redis optional unless the admin is slow or the site is large
ACF-heavy build with lots of dynamic queries
- Full page cache where possible
- Redis object cache
- Targeted use of
wp_cache_*for expensive repeated lookups - Review
wp_optionsautoload size and transient strategy
WooCommerce
- Page cache for non-commerce pages
- Redis object cache for product queries
- Strict bypass for cart, checkout, account, and sessions
- Careful purge rules for pricing and stock
When caching isn’t enough: PHP workers
Every request that misses the page cache needs a PHP worker. If all the workers are busy, new requests queue, and time to first byte climbs even though CPU can look fine.
Common causes of worker exhaustion:
- WooCommerce and logged-in traffic that bypasses the page cache
- slow database queries
- slow remote API calls
- heavy cron tasks running often
- admin use during peak traffic
A brochure site with solid page caching can run on few workers. WooCommerce and membership sites need more, because so much of their traffic can’t be cached. If your host shows worker metrics, watch them during peak traffic and deployments.
Memory matters too. When PHP workers and MySQL compete for RAM on a small server, a missing swap file can take MySQL down.
Quick checklist
- Confirm you have a working full page cache for anonymous users
- Confirm OPcache is enabled on the server
- If the site is dynamic, enable Redis and verify persistence
- Keep a hard “do not cache” list, enforced at plugin, server, and CDN
- Validate with headers (
X-Cache,cf-cache-status) and real tests - Watch PHP worker usage at peak times
Related reading
- Database optimisation techniques
- Advanced customisation with hooks
- WordPress for financial services: three myths
FAQ
Should I enable Redis on every WordPress site?
Not automatically. If you have a good full page cache and the site is small, Redis might not change much. It helps most on dynamic sites, ACF-heavy builds, multisite and WooCommerce.
Are transients “bad for performance”?
No. Used well, they save expensive work. Without Redis they live in wp_options, so keep them small and give each one an expiry.
Can I cache HTML at Cloudflare for WordPress?
Yes, but only with careful bypass rules for logged-in sessions and dynamic pages. If you run WooCommerce, be extra strict around carts, checkout, and account areas.
A cache that worked at launch can start serving stale or broken pages after a plugin update. Catching that before your visitors do is part of my WordPress maintenance work.
Published , updated .
Part 1 of WordPress Performance.