
Most “WordPress is slow” tickets aren’t database mysteries. They’re usually PHP doing the same work on pages that rarely change. This WordPress full-page caching tutorial shows how to add Nginx FastCGI cache on a VPS.
Anonymous visitors get cached HTML. Logged-in users, checkout flows, and admin pages stay dynamic.
You’ll do this with a few predictable config edits. You’ll also have a rollback you can run in minutes.
Examples assume Ubuntu 24.04+ with Nginx and PHP-FPM. The same approach maps cleanly to Debian 12+.
Before you start: what this cache does (and what it doesn’t)
Nginx FastCGI cache stores the generated response from PHP (usually HTML). On later requests, Nginx serves that response directly. PHP doesn’t run for repeat hits.
On small-to-mid VPS plans, this often cuts TTFB from 400–1200ms down to 30–150ms. It helps most during bot bursts and traffic spikes.
- Great for: home page, blog posts, category pages, marketing pages, most WooCommerce product pages (for logged-out visitors).
- Not for: logged-in sessions, wp-admin, cart/checkout, pages that must vary by user (unless you implement advanced vary logic).
- Doesn’t replace: browser caching, CDN, image optimization, or good PHP-FPM limits. It complements them.
If you need a solid base VPS for this setup (root access, predictable performance, and easy scaling), start with a HostMyCode VPS. If you’d rather not handle patching, backups, and hardening, use managed VPS hosting.
Prerequisites checklist (keep it boring and reliable)
- Ubuntu 24.04+ or Debian 12+
- Nginx 1.24+ (Ubuntu LTS repos are fine)
- PHP 8.2+ with PHP-FPM (8.3 also fine in 2026)
- A working WordPress site served by Nginx (not Apache)
- SSH access and sudo privileges
Step 1 — Confirm your current stack and baseline performance
Confirm versions first. Then confirm where your site config lives.
nginx -v
php-fpm8.3 -v 2>/dev/null || php-fpm8.2 -v
sudo systemctl status nginx --no-pager
sudo systemctl status php8.3-fpm --no-pager || sudo systemctl status php8.2-fpm --no-pager
Capture a baseline TTFB for an anonymous request. Run it a few times:
curl -s -o /dev/null -w "TTFB:%{time_starttransfer} Total:%{time_total}\n" https://example.com/
Then check the headers you already send. You’ll add cache headers shortly:
curl -I https://example.com/ | sed -n '1,20p'
If you’re still organizing a multi-site Nginx layout, align with our Nginx server blocks tutorial. It helps you avoid one oversized config file.
Those files are hard to review and risky to change.
Step 2 — Create a FastCGI cache zone on disk
Pick a cache path on fast storage (NVMe-backed VPS is ideal). A common, readable option is:
sudo mkdir -p /var/cache/nginx/fastcgi
sudo chown -R www-data:www-data /var/cache/nginx/fastcgi
Next, define the cache zone in /etc/nginx/nginx.conf, inside the http {} block.
If you prefer smaller diffs, put these lines in an include. Then reference that include from nginx.conf.
Option A (directly in nginx.conf):
sudo nano /etc/nginx/nginx.conf
http {
# ...existing config...
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=WORDPRESS:200m inactive=60m max_size=5g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# Helpful cache status header (can be removed later)
add_header X-Cache $upstream_cache_status always;
# ...existing config...
}
What these values mean:
keys_zone=WORDPRESS:200m: in-memory index for cached items. 100–300m is typical for one busy WP site.inactive=60m: purge items not accessed for 60 minutes (not the same as TTL).max_size=5g: hard cap so the cache can’t eat your disk.
Test syntax and reload:
sudo nginx -t
sudo systemctl reload nginx
Step 3 — Add caching rules to your WordPress Nginx server block
Open your site config (commonly under /etc/nginx/sites-available/):
sudo nano /etc/nginx/sites-available/example.com
You’ll add three things:
- A variable that decides whether to skip cache
- Rules to skip cache for logins, admin, preview, WooCommerce cart/checkout, and common cookies
- FastCGI cache directives inside the PHP location
3.1 Define a “skip cache” variable inside the server {} block:
# Default: cache is allowed
set $skip_cache 0;
3.2 Skip cache for URLs that must stay dynamic:
# Don't cache wp-admin, login, previews, and XMLRPC
if ($request_uri ~* "^/wp-admin/|^/wp-login\.php|preview=true|^/xmlrpc\.php") {
set $skip_cache 1;
}
# WooCommerce essentials
if ($request_uri ~* "^/(cart|checkout|my-account)/") {
set $skip_cache 1;
}
3.3 Skip cache when cookies imply a user-specific session:
# Logged-in users, commenters, or WooCommerce sessions
if ($http_cookie ~* "wordpress_logged_in_|comment_author_|woocommerce_items_in_cart|woocommerce_cart_hash|wp_woocommerce_session_") {
set $skip_cache 1;
}
3.4 Enable FastCGI cache for PHP requests. Find your PHP handler block. It typically looks like location ~ \.php$ { ... } or a location ~ \.php$ + include snippet.
Add these directives inside it:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock; # adjust if using 8.2
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 301 302 10m;
fastcgi_cache_valid 404 1m;
fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# Optional: avoid caching very large responses
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
Important pitfall: don’t treat FastCGI cache as “cache every PHP file.” You’re caching WordPress front-end pages.
The skip rules prevent cached logins, cached carts, and cached user sessions.
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
Step 4 — Verify cache hits (and confirm sensitive pages don’t cache)
Request the home page twice. The first request should be a miss. The second should be a hit.
curl -I https://example.com/ | grep -i x-cache
curl -I https://example.com/ | grep -i x-cache
Typical X-Cache values are MISS, HIT, or BYPASS.
Now confirm the “must be dynamic” paths don’t cache:
curl -I https://example.com/wp-login.php | grep -i x-cache
curl -I https://example.com/cart/ | grep -i x-cache
On these, you want BYPASS (or repeated MISS), not HIT.
Step 5 — Add a safe manual purge endpoint (no plugins required)
Nginx can purge cached content if built with the purge module. Many distro packages don’t ship it, though.
For small sites, a practical alternative is “purge by directory.” You clear the cache directory when you need fresh content immediately.
It’s blunt, but predictable. On most WordPress sites, the cache refills quickly.
5.1 Create a purge script that only root (or a trusted admin group) can execute:
sudo nano /usr/local/sbin/wp-fastcgi-cache-purge
#!/usr/bin/env bash
set -euo pipefail
CACHE_DIR="/var/cache/nginx/fastcgi"
if [[ ! -d "$CACHE_DIR" ]]; then
echo "Cache directory not found: $CACHE_DIR" >&2
exit 1
fi
# Remove files only inside cache dir
find "$CACHE_DIR" -type f -delete
echo "Purged FastCGI cache at $CACHE_DIR"
sudo chmod 750 /usr/local/sbin/wp-fastcgi-cache-purge
sudo chown root:root /usr/local/sbin/wp-fastcgi-cache-purge
5.2 Tie it to a systemd unit so you can run it consistently and get logs:
sudo nano /etc/systemd/system/wp-fastcgi-cache-purge.service
[Unit]
Description=WordPress FastCGI cache purge
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/wp-fastcgi-cache-purge
sudo systemctl daemon-reload
Run a purge:
sudo systemctl start wp-fastcgi-cache-purge
sudo journalctl -u wp-fastcgi-cache-purge --no-pager
If you later need selective purging, use an Nginx build that supports cache purge. Or switch to a cache design that uses map rules and predictable cache keys per URL.
Keep it simple until you have a real reason not to.
Step 6 — Make WordPress play nicely (headers, cron, and admin workflows)
You’re caching at the web server, not in WordPress. That’s the point.
It also means changes may not show up to anonymous visitors until the TTL expires or you purge.
- Publishing cadence: if you publish often, set cache validity to 2–5 minutes. For a mostly-static marketing site, 10–30 minutes is usually fine.
- Admin verification: check edits in an incognito window. If you need the public view updated now, purge.
- WP-Cron: caching can mask symptoms like “scheduled post didn’t publish.” If your store relies on timely tasks, move to a real cron job. Follow our WordPress cron troubleshooting tutorial to replace WP-Cron with server cron safely.
Step 7 — Add guardrails: limits, timeouts, and “don’t cache this” patterns
Full-page caching usually fails in two ways. Either something user-specific gets cached, or the cache grows without bounds.
You already set max_size. Now add a few conservative exclusions.
7.1 Don’t cache POST requests. FastCGI cache typically caches GET/HEAD only, but make it explicit:
if ($request_method = POST) {
set $skip_cache 1;
}
7.2 Avoid caching query-string heavy endpoints used for tracking or previews:
if ($query_string ~* "(utm_|gclid=|fbclid=)") {
set $skip_cache 1;
}
If you decide later that UTM-tagged URLs should still cache, you can normalize the cache key. Starting conservative helps prevent “campaign link shows odd content” support tickets.
7.3 Keep timeouts sensible so a slow upstream doesn’t tie up workers:
location ~ \.php$ {
# ...existing...
fastcgi_read_timeout 60s;
}
If you hit timeouts regularly, fix the cause (PHP-FPM pool limits, slow plugins, remote API calls). Don’t just stretch the timeout to 180 seconds and hope.
Step 8 — Measure the win (TTFB, PHP load, and cache ratio)
Re-test TTFB. Run it twice:
curl -s -o /dev/null -w "TTFB:%{time_starttransfer} Total:%{time_total}\n" https://example.com/
curl -s -o /dev/null -w "TTFB:%{time_starttransfer} Total:%{time_total}\n" https://example.com/
Watch PHP-FPM while you load a few pages in an incognito window (cached). Then compare that to browsing as a logged-in user (uncached):
sudo systemctl status php8.3-fpm --no-pager || sudo systemctl status php8.2-fpm --no-pager
sudo journalctl -u php8.3-fpm -n 50 --no-pager 2>/dev/null || true
If you want real visibility (CPU, memory, disk, plus external checks), pair this with our VPS monitoring setup tutorial. Caching reduces load, but you still need alerts when a plugin update spikes PHP.
Step 9 — Troubleshooting: quick diagnostics for the common breakages
These are the issues you’ll actually run into. Each section starts with checks that fix most cases.
Problem: logged-in users see cached pages
- Confirm your cookie match works. Grab cookies from a logged-in browser session (dev tools) and make sure they match your regex.
- Make sure the cookie
ifblocks are inside the correctserver {}block for that domain. - Verify you didn’t override
$skip_cachein an included snippet.
Problem: WooCommerce cart/checkout is broken
- Confirm
/cart/,/checkout/, and/my-account/are bypassed. - Ensure the WooCommerce session cookie pattern includes
wp_woocommerce_session_. - Clear cache once after rule changes:
sudo systemctl start wp-fastcgi-cache-purge.
Problem: cache never hits
- Check the
X-Cacheheader. If it’s alwaysBYPASS, your skip rules are too broad. - Look for cookies set for all visitors (some plugins set cookies unconditionally). Those will trigger bypass if your cookie regex is too aggressive.
- Check permissions on
/var/cache/nginx/fastcgi. Nginx must be able to write there.
Problem: you get random 500s after enabling cache
- Inspect the Nginx error log:
sudo tail -n 80 /var/log/nginx/error.log - Confirm your PHP socket path:
/run/php/php8.3-fpm.sockvsphp8.2-fpm.sock - If disk is near full, Nginx can fail to write cache files. Use our VPS disk space troubleshooting tutorial to fix it quickly.
Step 10 — Hardening notes: don’t let performance changes create security gaps
Full-page caching sits directly on the request path. Treat it like any other production change.
Test it, verify it, and keep the blast radius small.
- TLS first: a fast site with sloppy HTTPS is still a problem. Follow our TLS hardening tutorial for modern ciphers, headers, and safe reloads.
- Rate-limit noisy endpoints: caching helps with load, but it won’t stop brute-force attempts. Consider Nginx rate limiting for
/wp-login.phpand XML-RPC if you keep it enabled. - Keep backups boring: caching isn’t a backup. If the VPS dies, the cache disappears. Maintain offsite backups and test restores.
Step 11 — Rollback plan (so you can undo this cleanly)
If anything looks off, roll back in a controlled way:
- Remove the FastCGI cache directives from the PHP
locationblock. - Remove (or comment) the
fastcgi_cache_pathlines fromnginx.conf. - Reload Nginx:
sudo nginx -t && sudo systemctl reload nginx - Optionally delete cache:
sudo rm -rf /var/cache/nginx/fastcgi
This guide avoids custom modules, so rollback is just config edits and a reload.
Summary: a practical caching setup you can support
This WordPress full-page caching tutorial aims for the boring, supportable middle. Anonymous pages get cheap. Logged-in workflows stay correct.
You also have a manual purge for publishing and troubleshooting.
If you want more performance headroom without wrestling with shared-host limits, move WordPress to a HostMyCode VPS (or choose managed VPS hosting if you prefer a supported stack). Caching works best when the underlying server is stable and monitored.
If you’re on shared hosting and traffic spikes keep slamming CPU limits, full-page caching on Nginx is one of the quickest fixes. HostMyCode can provision a clean VPS ready for Nginx + PHP-FPM, or you can hand ongoing updates and hardening to our managed VPS hosting team. Start with a HostMyCode VPS and scale as your cache hit rate improves.
FAQ
Will FastCGI cache break WordPress admin?
Not if you bypass /wp-admin/ and /wp-login.php and skip cache when wordpress_logged_in_ cookies exist. Verify with curl -I and the X-Cache header.
Can I use this with WooCommerce?
Yes for logged-out browsing. You must bypass cache for /cart/, /checkout/, and /my-account/, plus WooCommerce session cookies. If you run membership pricing or personalized catalog pages, keep those endpoints uncached.
How do I clear the cache after publishing?
Use the included systemd purge service: sudo systemctl start wp-fastcgi-cache-purge. It clears the cache directory so new content appears immediately to anonymous visitors.
Should I still use a CDN?
Yes. FastCGI cache reduces PHP load and improves TTFB; a CDN cuts latency for distant visitors and offloads static assets. If your audience is spread out geographically or your pages are image-heavy, use both.
How long should I cache pages?
Start with 10 minutes for 200/301/302 responses. If you publish frequently, drop to 2–5 minutes and use manual purges during major updates.