A practical guide to WordPress caching that actually holds up in production: page caching, Redis object cache, transients, OPcache, CDN edge caching, and safe purge rules.
Caching is the cornerstone of a high-performance WordPress site, but only when you understand which layer is doing what. Many slow sites have “caching enabled” yet still grind because the cache is misconfigured, purging too aggressively, or caching the wrong things.
This guide breaks down the caching layers that matter, shows where they fit in a typical stack, and calls out the gotchas that catch real projects.
Why Caching Is a System, Not a Plugin
Caching is about reducing repeated work, not chasing a score. In WordPress, the biggest performance gains come from preventing PHP and database execution entirely where possible.
Poor caching strategies usually fail because:
- Cache layers overlap without clear ownership
- Invalidation rules are unclear or unsafe
- Logged-in and dynamic routes are cached accidentally
- Caches hide underlying data or query problems
Good caching reduces load and improves predictability under traffic spikes.
Design Rationale and Trade-offs
Page caching beats object caching for anonymous traffic
If the page never hits PHP, you win. Object caching helps dynamic routes, but cannot replace full-page caching.
Invalidation is harder than storage
Caching data is easy. Knowing when to expire it is the real engineering work.
Edge caching shifts risk
CDNs reduce origin load, but mistakes propagate faster. Bypass rules matter more than hit rate.
Practical Guardrails
- Never cache logged-in, cart, or account routes
- Treat purge rules as production-critical code
- Measure TTFB under load, not just cold load
Key Takeaways
- Cache the whole response first
- Invalidate aggressively and deliberately
- Avoid stacking caches blindly
What you will learn
- 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 realistic layered strategy you can use on 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
The goal is not “maximum caching”. The goal is fast, correct, predictable behaviour.
1. Page caching (full HTML)
Page caching stores a rendered HTML response and serves it back without running WordPress at all. This is usually the biggest win for time to first byte.
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 (excellent defaults and cache invalidation)
- WP Rocket (very popular and broadly compatible)
- Cache Enabler (lightweight, fewer bells and whistles)
- Server-level FastCGI cache (NGINX) for high throughput and low PHP load
NGINX FastCGI cache example
This is the direction you want for server-level page caching. It caches the output of PHP-FPM responses, rather than trying to cache “.html files”.
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=1g;
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;
}
}
That “skip when logged in” pattern is essential if you ever use cache-everything rules at the edge too.
Purge rules you actually need
A cache that never purges will eventually serve stale content. A cache that purges constantly is barely a cache.
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 modern caching plugins handle this well. If you build your own, wire into save_post, menu updates, and WooCommerce 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 is only in-memory for that single request. You want persistence for busy sites.
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.
Recommended approach
- Redis is the common choice for WordPress hosting and is well supported.
- Use the Redis Object Cache plugin (or your host’s integrated solution).
- Confirm the cache is actually persistent by checking the admin page or output headers.
Using the Object Cache API in custom code
Prefer group keys, sensible expirations, and cache invalidation over “cache 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. cite
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. If you see a large autoload size, review what is being stored and whether it should autoload. cite
Rule of thumb:
- transients are great for caching computed results
- do not use transients as a dumping ground for huge objects
- invalidate aggressively when correctness matters (pricing, stock, 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. cite
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 is essential for global performance and protecting your origin.
Cache static assets at the edge:
- images
- CSS and JS
- fonts (with correct CORS headers)
For HTML edge caching, you must be conservative. “Cache Everything” can be fantastic for content pages, but it must be paired with bypass rules for logged-in sessions and dynamic endpoints. cite
Cloudflare rule patterns worth using
- 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
This is the simple layer that 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 - Ensure your build pipeline fingerprints assets (or uses query string versioning at minimum)
Most performance plugins can help, but you should also validate with DevTools and a Lighthouse pass.
7. Cache invalidation and “don’t cache this” lists
If your cache serves the wrong content even once, trust is damaged. The risk increases with:
- WooCommerce
- membership content
- personalisation (greetings, pricing, location-based content)
- forms, previews, search, and logged-in toolbars
Create a clear list of 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. A layered caching strategy that works
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 and fragments
- Strict bypass for cart, checkout, account, and sessions
- Careful purge rules for pricing and stock
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
Related reading
- Database Optimisation Techniques
- Hook System: Advanced Customisation
- WordPress for Financial Services: Busting the 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 shines on dynamic sites, ACF-heavy builds, multisite, and WooCommerce.
Are transients “bad for performance”?
No. Misused transients are bad. Used correctly, they reduce expensive work. Without Redis, keep them small and avoid dumping huge arrays into wp_options.
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.
Need WordPress support? I provide maintenance and development for businesses across Cheshire. Learn more about my WordPress services or get in touch.