I've watched hundreds of WordPress sites scale from low traffic to real production load. The caching patterns that break always break the same way, and the ones that survive all follow a few non-negotiable rules.
Most caching advice treats WordPress like a generic PHP app. It isn't. The plugin ecosystem, the admin interface, the way themes interact with data—all of it creates edge cases that generic cache rules will miss. You need layers that understand WordPress, not just HTTP.
The three-layer approach that survives production
WordPress caching splits into three distinct layers, each solving a different problem. Skip one and you either leave performance on the table or you cache something that should never be cached.
Full-page cache: the only layer that matters for anonymous traffic
Full-page caching serves pre-rendered HTML for logged-out visitors. This is where you win or lose on page speed. A proper full-page cache bypasses PHP entirely—WordPress never boots, the database never wakes up, and you serve static files at near-CDN speeds.
Nginx FastCGI cache and Varnish are the production standards here. They sit in front of PHP-FPM and intercept requests before WordPress sees them. Cache plugins like WP Rocket and LiteSpeed Cache do the same job at the application layer, writing static HTML to disk and serving it via rewrite rules.
The choice matters less than your purge strategy. When you update a post, the homepage needs to invalidate. When you publish a new post, archive pages need to clear. When you change a menu, every page using that menu needs a refresh. Get the purge logic wrong and visitors see stale content for hours.
I prefer FastCGI cache on Nginx for VPS deployments because it's fast, reliable, and you control purge behavior with a small Nginx config and the Nginx Helper plugin. No third-party service, no external dependencies.
fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header updating http_500;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
Set $skip_cache to 1 for logged-in users, POST requests, and query strings. Cache everything else aggressively.
Object cache: where database queries go to retire
Object caching stores the results of expensive database queries in memory. WordPress makes dozens of queries per page load by default—user data, options, post metadata, taxonomy terms. Without object cache, every page hit runs the same queries over and over.
Redis is the right answer here. Memcached works too, but Redis handles persistence better and the WordPress community has settled on it. Install Redis on localhost, add the Redis Object Cache plugin, drop object-cache.php into wp-content, and you're done.
sudo apt install redis-server php-redis
sudo systemctl enable redis-server
The object cache only helps logged-in users and the admin dashboard because full-page cache already handles anonymous traffic. But it matters a lot there—admin-ajax calls get faster, the block editor feels snappier, and your database stops sweating under editorial workloads.
Don't bother with object cache on tiny shared hosting. You need at least 512 MB of dedicated RAM to make Redis worthwhile, and shared environments usually won't give you the socket access you need anyway.
Browser cache: let the client do the work
Browser caching tells visitors to keep copies of your CSS, JavaScript, images, and fonts locally. This is basic HTTP stuff—Cache-Control headers with reasonable max-age values.
Most caching plugins set these headers automatically. If you're managing Nginx yourself, set them explicitly:
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
One year is aggressive but safe for versioned assets. WordPress themes and plugins that enqueue scripts properly will append version strings to filenames, so cache busting happens automatically when you update.
Anti-patterns that create 3 AM incidents
Certain caching patterns look fine in staging and explode in production. You'll see them recommended in blog posts from 2018 that haven't aged well.
Caching logged-in users at the full-page layer
Some plugins promise to cache pages for logged-in users by varying cache keys on user role or session ID. This sounds clever until you realize you're now storing a separate cached copy per user. A site with 500 concurrent logged-in users needs 500 cached versions of every page.
You run out of disk or RAM fast. Worse, purging becomes a nightmare—do you purge all user caches when a post updates? Just the affected roles? The logic gets complicated and the performance gains evaporate.
Instead, skip full-page cache entirely for logged-in users and rely on object cache to speed up queries. The admin bar and personalized elements make full-page caching for authenticated sessions a bad trade anyway.
Caching pages with forms or CSRF tokens
Contact forms, login pages, checkout flows—all generate unique tokens to prevent CSRF attacks. Cache those pages and the token becomes stale. Submissions fail, users see security errors, and your inbox fills with "the form is broken" tickets.
Exclude any URL with a form from full-page cache. Most plugins let you blacklist paths. Add /contact/, /checkout/, /wp-login.php, and any other interactive endpoints to the exclusion list.
If you're running WooCommerce, disable full-page cache site-wide or get very precise with your exclusions. Cart state and session data make aggressive caching dangerous on ecommerce sites.
Using a cache plugin and a server-level cache simultaneously
Running WP Rocket on a server that already has FastCGI cache or LiteSpeed cache creates two caching layers that don't know about each other. When you purge one, the other still serves stale content. Debugging becomes impossible because you're never sure which layer is causing the problem.
Pick one and commit. If your host provides server-level caching (LiteSpeed, Nginx FastCGI, Varnish), use that and skip the plugin. If you're on generic Apache hosting, use a plugin. Never stack them.
What to cache, what to skip
WordPress has a handful of URLs you should never cache and a few you absolutely must.
Always cache: - Blog posts and pages (the entire point) - Archive pages (category, tag, date) - The homepage (unless it's highly personalized) - Static assets (CSS, JS, images, fonts) - RSS feeds (with a short TTL, maybe 5 minutes)
Never cache:
- /wp-admin/ and /wp-login.php
- Any URL with a query string (unless you whitelist safe params)
- POST requests
- Pages with cookies indicating logged-in state
- AJAX endpoints (/wp-admin/admin-ajax.php)
- REST API endpoints if they return user-specific data
- WooCommerce cart, checkout, and account pages
Some themes and plugins add dynamic widgets or personalized content to otherwise static pages. A "recent posts" widget populated via AJAX is fine to cache. A widget showing the logged-in user's name is not. Test carefully.
So how do you actually purge when content changes?
Purging is where most caching strategies fall apart. WordPress fires hooks when content changes—save_post, edit_term, update_option—and your cache layer needs to listen and respond.
Nginx Helper (for FastCGI cache) and LSCache (for LiteSpeed) handle this automatically. They hook into WordPress and send purge requests to the cache backend when content updates. WP Rocket and similar plugins do the same at the application layer.
The default purge behavior is usually too aggressive (clear everything on every change) or too conservative (only purge the exact post that changed). Tune it based on your site structure:
- Purge the homepage, relevant category archives, and the updated post itself when you edit a post.
- Purge all archives when you add a new post.
- Purge everything if you change a global setting like site title or menu.
I've seen sites cache for 24 hours with manual purge on deploy. That works for marketing sites with infrequent updates but not for active blogs or news sites.
Cache warming: optimization or premature optimization?
Cache warming pre-generates cached pages by crawling your site after a purge. Some plugins offer this as a feature. It sounds useful—why wait for a real user to trigger cache generation?
In practice, cache warming is only valuable for high-traffic sites where the first visitor after a purge would experience a slow page load that hurts conversion or engagement. For most blogs and small business sites, it's overkill. The first visitor hits a cold cache, waits an extra second, and then everyone else benefits.
If you do warm the cache, crawl only your most important pages. Warming thousands of low-traffic archive pages just wastes CPU and fills your cache with content nobody visits.
Monitoring: how to know your cache is actually working
You can't manage what you don't measure. Check cache hit rates regularly to confirm your config is doing something.
For Nginx FastCGI cache, add this to your server block:
add_header X-FastCGI-Cache $upstream_cache_status;
Then inspect response headers in your browser's developer tools. You should see X-FastCGI-Cache: HIT on cached pages and MISS or BYPASS on uncached ones. A hit rate below 70% means your exclusion rules are too broad or your cache is expiring too quickly.
Redis cache hit rates are visible in the Redis Object Cache plugin dashboard. Expect 90%+ hit rates for a healthy object cache. Lower than that and either your cache size is too small or you're purging too aggressively.
CDN or no CDN?
Cloudflare and other CDNs add another caching layer in front of your origin server. They're worth it for sites with global traffic or heavy media libraries, but they complicate purging.
When you update a post, you purge your origin cache and then purge Cloudflare's edge cache. Miss one and visitors see stale content until the CDN's TTL expires. Most WordPress cache plugins have Cloudflare integration that handles this automatically, but you need to configure it and test it.
For small sites hosted in a single region serving a local audience, the complexity usually isn't worth it. Focus on origin caching first.
What production caching really needs
WordPress caching works when you layer it correctly and purge it intelligently. Full-page cache carries your anonymous traffic, object cache smooths out the database, and browser cache keeps repeat visitors fast. Stack them properly, exclude the right URLs, and test your purge logic before you push to production.
The plugins and servers change, but the principles don't. Cache aggressively where it's safe, bypass where it's not, and measure everything so you know when something breaks.
