
A properly configured Nginx cache can take a WordPress site from “fine” to consistently fast. It does that by keeping PHP out of the request path for anonymous visitors.
The hard part isn’t enabling caching. The hard part is avoiding the traps: cached logins, stale carts, and the classic “why is the homepage showing the wrong thing?” moment. This Nginx cache setup guide tutorial shows how to set up FastCGI caching on a Linux VPS, add sane bypass rules, purge safely, and confirm what Nginx is actually serving.
You’ll end up with:
- FastCGI cache for PHP-FPM (WordPress front-end)
- Explicit cache bypass for wp-admin, logged-in users, previews, and WooCommerce sessions
- Headers you can use to debug HIT/MISS/BYPASS
- A safe purge method (no random “rm -rf cache” in production)
Before you start: what this tutorial assumes
This guide targets Ubuntu 24.04/24.10 or Debian 12/13 running Nginx 1.24+ and PHP 8.2/8.3 with PHP-FPM. The same approach works on AlmaLinux/Rocky with a few path differences. The commands here follow Ubuntu/Debian conventions.
Prerequisites:
- Root or sudo access to your VPS
- A working WordPress site already served by Nginx + PHP-FPM
- Enough disk for cache (start with 1–5 GB)
If you’re still building the base stack (or you haven’t set up SSL yet), do that first. HostMyCode customers often start from a clean VPS and layer on Nginx + PHP-FPM.
If you want a pre-tuned baseline and someone to review your config, managed VPS hosting gets you there faster.
Step 1 — Confirm your PHP-FPM socket and Nginx site layout
Start by confirming how Nginx connects to PHP-FPM. It can use a Unix socket or TCP. On Ubuntu/Debian, it’s usually a socket under /run/php:
ls -la /run/php/
Common results:
/run/php/php8.3-fpm.sock/run/php/php8.2-fpm.sock
Next, locate your server block. Typical locations:
/etc/nginx/sites-available/example.com/etc/nginx/sites-enabled/example.com
Do a quick health check before you touch anything:
sudo nginx -t
sudo systemctl status nginx --no-pager
Step 2 — Create a FastCGI cache zone (global Nginx config)
Create a dedicated cache directory. Then make sure Nginx can write to it:
sudo mkdir -p /var/cache/nginx/fastcgi
sudo chown -R www-data:www-data /var/cache/nginx/fastcgi
Now define the cache path and zone. Add the following inside the http {} block in /etc/nginx/nginx.conf (or an included file like /etc/nginx/conf.d/cache.conf):
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=5g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
What those settings do:
keys_zone=WORDPRESS:100mstores cache metadata in RAM. 100 MB covers many small and mid-sized sites.inactive=60mdrops cache entries that haven’t been requested for 60 minutes.max_size=5gcaps disk usage. Start smaller; increase if you see frequent churn.
Reload after changes:
sudo nginx -t && sudo systemctl reload nginx
Step 3 — Add cache bypass logic (the part that saves you)
For WordPress, “enable caching” really means “cache only what’s safe.” You’ll move faster if you define one variable. Then everything keys off it.
Inside your server block (server {}) add:
# Default: cache is allowed
set $skip_cache 0;
# Don’t cache admin, login, XML-RPC
if ($request_uri ~* "^/wp-admin/|^/wp-login\.php|^/xmlrpc\.php") {
set $skip_cache 1;
}
# Don’t cache previews, feeds, or REST API responses you expect to be dynamic
if ($query_string ~* "preview=true|preview_id|preview_nonce") {
set $skip_cache 1;
}
# Don’t cache if WordPress says the user is logged in or has a comment cookie
if ($http_cookie ~* "wordpress_logged_in_|comment_author") {
set $skip_cache 1;
}
# WooCommerce: avoid caching sessions/cart/checkout
if ($request_uri ~* "^/cart/|^/checkout/|^/my-account/") {
set $skip_cache 1;
}
if ($http_cookie ~* "woocommerce_items_in_cart|woocommerce_cart_hash|wp_woocommerce_session") {
set $skip_cache 1;
}
These rules are conservative on purpose. Get a safe baseline working first. Tighten later after you verify behavior.
If you run membership, personalization, or language-switch plugins that store state in cookies, add their cookie patterns to the bypass list.
If you want a broader hardening baseline (permissions, wp-config safety, admin access controls), keep a plan for the day performance issues aren’t performance issues. The workflow in this VPS incident response tutorial is useful when traffic spikes come from bad bots or injected code.
Step 4 — Enable FastCGI caching in the PHP location
Find your PHP handler block. It often looks like this:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Update it to enable caching. Also add a header you can inspect:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
# Cache only GET/HEAD by default
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 301 302 10m;
fastcgi_cache_valid 404 1m;
# Respect the bypass variable
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# Avoid thundering herd on expiry
fastcgi_cache_lock on;
fastcgi_cache_lock_timeout 5s;
# Serve stale on upstream hiccups (keeps your site up during brief PHP restarts)
fastcgi_cache_use_stale updating error timeout invalid_header http_500 http_503;
# Debug header
add_header X-FastCGI-Cache $upstream_cache_status always;
}
Reload Nginx:
sudo nginx -t && sudo systemctl reload nginx
Now test a page and look for the cache header:
curl -I https://example.com/ | grep -i fastcgi
Expected progression:
- First request:
X-FastCGI-Cache: MISS - Second request:
X-FastCGI-Cache: HIT
If you always see BYPASS, one of your $skip_cache conditions is triggering. If you always see MISS, check permissions. Also confirm Nginx can write to /var/cache/nginx/fastcgi.
Step 5 — Prevent caching of admin assets, uploads, and static files the right way
FastCGI cache is for PHP responses. Static files should be served directly with long browser caching headers.
Add (or confirm) a static cache policy for common asset types:
location ~* \.(css|js|jpg|jpeg|png|gif|webp|svg|ico|woff2?)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
access_log off;
try_files $uri =404;
}
Two pitfalls to avoid:
- Don’t set a 30-day browser cache on
.phpoutput; FastCGI cache already covers page responses server-side. - Don’t cache
/wp-admin/. You’ll eventually serve the wrong nonce and break the dashboard.
Step 6 — Add a safe purge mechanism (without opening a security hole)
Nginx Open Source doesn’t include a built-in cache purge endpoint. On a VPS, you usually pick one of these:
- Soft purge: rotate cache keys by changing the key (fast and safe, but you lose a warm cache).
- Filesystem purge: delete cache files deliberately (simple, but you need guardrails).
- Nginx cache purge module: doable, but rarely worth it unless you already compile Nginx.
For most WordPress VPS setups, filesystem purge is the cleanest approach. Create a root-only script:
sudo tee /usr/local/sbin/purge-fastcgi-cache > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
CACHE_DIR="/var/cache/nginx/fastcgi"
if [[ ! -d "$CACHE_DIR" ]]; then
echo "Cache dir not found: $CACHE_DIR" >&2
exit 1
fi
# Refuse to run if path looks wrong
if [[ "$CACHE_DIR" == "/" || "$CACHE_DIR" == "/var" || "$CACHE_DIR" == "/var/cache" ]]; then
echo "Refusing to purge an unsafe path: $CACHE_DIR" >&2
exit 2
fi
echo "Purging FastCGI cache in $CACHE_DIR ..."
find "$CACHE_DIR" -type f -delete
echo "Done."
EOF
sudo chmod 750 /usr/local/sbin/purge-fastcgi-cache
sudo chown root:root /usr/local/sbin/purge-fastcgi-cache
Run it after a major theme change or template change. It’s also useful when you need a clean slate:
sudo /usr/local/sbin/purge-fastcgi-cache
If you want a safer operational pattern, treat changes like something you might need to undo quickly. The runbook approach in this disaster recovery tutorial fits caching work well, because failures are obvious and usually time-sensitive.
Step 7 — Add WordPress-side signals (so Nginx knows when not to cache)
Nginx can’t see WordPress “state” unless it shows up in cookies, headers, or URLs. You already covered the common cookies.
One more practical trick: allow an explicit bypass signal for debugging.
Add this inside your server block:
# Allow explicit bypass via header (useful for debugging)
if ($http_x_skip_cache = "1") {
set $skip_cache 1;
}
Now you can force a bypass from your terminal:
curl -I -H "X-Skip-Cache: 1" https://example.com/
If your plugin sets DONOTCACHEPAGE via a cookie or header, mirror that behavior here by matching its cookie name.
Step 8 — Verify correctness with a small test checklist
Speed is easy to measure. Correctness takes a little discipline.
Run this checklist before you call the cache “production-ready.”
- Homepage: first request MISS, second request HIT.
- A blog post: HIT on repeat, correct featured image and styling.
- Logged-in user: BYPASS and no “back button shows logged-out page” weirdness.
- wp-admin: BYPASS, no cached admin pages.
- WooCommerce cart/checkout: BYPASS and totals update correctly.
- Preview links: BYPASS and shows draft content.
Useful one-liners:
# Check cache status header
curl -I https://example.com/ | egrep -i 'x-fastcgi-cache|cache-control'
# See if cookies are causing bypass
curl -I https://example.com/ -H 'Cookie: wordpress_logged_in_test=1'
Step 9 — Debug common “why is it still slow?” scenarios
FastCGI cache only helps with anonymous PHP responses. If CPU is still high or TTFB stays slow, the bottleneck is usually elsewhere.
Scenario A: You always get MISS
- Confirm Nginx can write the cache directory:
sudo -u www-data test -w /var/cache/nginx/fastcgi && echo ok - Check you aren’t bypassing everything with overly broad cookie matches.
- Verify the directives are in the loaded config:
sudo nginx -T | grep -n fastcgi_cache
Scenario B: You get HIT, but pages look “wrong”
- You’re caching personalized output. Add the plugin’s cookie name to the bypass rules.
- Your theme varies output based on query parameters. Bypass those query params as needed.
Scenario C: Admin complains about updates not showing
- Cache serves older pages until the TTL expires. Either lower
fastcgi_cache_valid(for example, 2–5 minutes) or purge after updates. - If you publish frequently, keep posts on a shorter TTL and leave the homepage moderate.
Scenario D: SSL redirect loops or mixed content
This is rarely caused by caching, but it often appears after config edits. Confirm SSL and redirects are correct end-to-end.
If you need a clean reference for Nginx + Let’s Encrypt and safe redirects, use this SSL setup guide tutorial.
Step 10 — Operational hardening: logs, limits, and safe rollbacks
Caching can hide problems until a spike hits. It can also hide them until an edge case shows up. A few small safeguards make troubleshooting much faster.
Log cache status for a short diagnostic window
Temporarily add a log format that includes cache status. In /etc/nginx/nginx.conf inside http {}:
log_format cachelog '$remote_addr - $host "$request" $status '
'cache=$upstream_cache_status rt=$request_time urt=$upstream_response_time';
Then in your server block:
access_log /var/log/nginx/access-cache.log cachelog;
Reload Nginx and inspect:
sudo tail -n 50 /var/log/nginx/access-cache.log
Once you’re done troubleshooting, switch back to your normal access log to reduce noise.
Keep a rollback copy of your site config
sudo cp /etc/nginx/sites-available/example.com /etc/nginx/sites-available/example.com.bak.$(date +%F)
If you manage multiple sites or client accounts, predictable rollbacks matter. That’s one reason many agencies run a HostMyCode VPS per client tier (or per site cluster): configuration drift stays contained.
Performance expectations (realistic numbers)
On a typical WordPress site, FastCGI caching often drops TTFB for anonymous requests from 400–1200 ms to 30–150 ms on the same VPS.
Nginx serves cached HTML directly from disk (and the OS page cache). It does that instead of starting PHP and booting WordPress. CPU usage also tends to flatten under traffic spikes, because cache HITs don’t execute PHP.
Logged-in dashboards and checkout flows won’t improve nearly as much. They still run PHP by design, and that’s expected.
Summary: the “safe default” caching model for WordPress on a VPS
- Cache anonymous GET/HEAD requests through FastCGI cache.
- Bypass for wp-admin, wp-login, previews, and user/session cookies.
- Expose
X-FastCGI-Cacheand verify HIT/MISS with curl before you trust it. - Purge with a root-only script, not a public endpoint.
If you want caching you can rely on under load, make sure you have stable CPU and disk I/O first. Your hosting tier shows up quickly here.
For production WordPress sites, start with a managed VPS hosting plan if you want changes reviewed and monitored. Or use a HostMyCode VPS if you prefer full control in a predictable Linux environment.
If you’re implementing FastCGI caching to keep WordPress responsive during traffic spikes, run it on a VPS with consistent disk I/O and enough headroom for PHP-FPM. HostMyCode offers HostMyCode VPS plans for hands-on admins and managed VPS hosting if you’d rather have the stack maintained and monitored.
FAQ
Will FastCGI cache break WooCommerce?
Not if you bypass cache for cart/checkout/account URLs and WooCommerce session cookies. Start conservative. If anything feels “sticky,” expand the cookie bypass list.
How do I tell if Nginx is serving cached HTML?
Check the response header: X-FastCGI-Cache: HIT. If you don’t see it, the config isn’t active or your requests are being bypassed.
Should I still use a WordPress cache plugin?
Often no for page caching, since Nginx is doing it at the edge of your stack. Many sites still use plugins for object cache integration, minification, or image optimization, but avoid double page-caching layers.
What’s the safest way to purge cache after updates?
Use a root-only purge script that deletes files inside /var/cache/nginx/fastcgi after validating the path. Don’t expose a public “/purge” endpoint unless you fully lock it down.
What if my site is behind Cloudflare or another CDN?
Nginx FastCGI cache still helps because it reduces origin load for cache misses and logged-out traffic that reaches your server. Just be careful when debugging: CDN caching can mask your origin HIT/MISS signals.