
Most “WordPress is slow” tickets trace back to one issue. The server repeats the same PHP work for the same anonymous page views. This Nginx caching tutorial shows how to add a small, predictable FastCGI microcache in front of PHP-FPM on an Ubuntu VPS.
You’ll also set up a safe purge workflow so changes show up quickly.
You’ll end up with a configuration you can drop into /etc/nginx. You’ll also get a bypass strategy that keeps wp-admin and logged-in sessions correct. Finally, you’ll add a purge endpoint restricted to your IP.
This is written for real hosting conditions. Think plugin-heavy sites, crawler spikes, and needing to prove what changed during troubleshooting.
What you’ll build (and what you won’t)
- Microcache for anonymous traffic (typical TTL: 10–60 seconds). It absorbs bursts and cuts PHP load fast.
- Bypass rules for logged-in users, WooCommerce carts, previews, and admin endpoints.
- Safe cache purge via an internal-only Nginx location (no public “purge all”).
- Visibility using cache status headers and log fields so you can verify behavior.
You won’t set up a full-page cache plugin stack, a CDN, or Redis object caching here.
Those can help, but this stays focused on server-side Nginx caching you can inspect and control.
Prerequisites (Ubuntu VPS + Nginx + PHP-FPM)
This guide assumes Ubuntu Server on a VPS (or dedicated server) and a standard Nginx + PHP-FPM WordPress setup:
- Ubuntu 24.04/26.04 LTS-style environment
- Nginx 1.24+ (common in Ubuntu repos; if you run newer mainline, the config still applies)
- PHP-FPM 8.2 or 8.3
- WordPress running from a vhost like
/etc/nginx/sites-available/example.com
If you’re deciding where to run this, a VPS with NVMe storage makes microcache feel instant.
If you want the OS and web stack maintained for you, managed VPS hosting is the cleanest path. If you prefer full control, start with a HostMyCode VPS and apply the steps below.
Step 1: Baseline your current performance (before caching)
Grab a baseline before you touch Nginx. It gives you a reference point and makes regressions obvious.
-
Check that you can reach your site and get consistent timings:
curl -s -o /dev/null -w "ttfb:%{time_starttransfer} total:%{time_total}\n" https://example.com/ -
Confirm PHP requests actually hit PHP-FPM (not a static page):
curl -I https://example.com/ | sed -n '1,20p' -
Watch PHP-FPM and Nginx load during a small burst (run on the server):
sudo ss -s sudo systemctl status php8.3-fpm --no-pager sudo tail -f /var/log/nginx/access.log
Save those numbers.
A 10–60s microcache often shifts anonymous TTFB from “PHP speed” to “Nginx speed.”
It also smooths CPU spikes during crawlers or social bursts.
Step 2: Create the FastCGI cache path with correct ownership
Nginx needs a local directory for cached responses.
Pick a path that’s easy to find, easy to clean, and unlikely to collide with other tooling.
sudo mkdir -p /var/cache/nginx/fastcgi
sudo chown -R www-data:www-data /var/cache/nginx/fastcgi
sudo chmod 700 /var/cache/nginx/fastcgi
Why 700? Nginx (running as www-data) can read and write cache files. Other users can’t.
Step 3: Add a cache zone in nginx.conf
Edit /etc/nginx/nginx.conf and define the cache zone inside the http {} block.
Keep it near other global settings so you can find it later.
sudo nano /etc/nginx/nginx.conf
Add:
http {
# ... existing config ...
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=WORDPRESS:200m inactive=60m max_size=5g;
# Optional: keep cache keys stable across schemes (http/https) if you force https anyway
# map $scheme $cache_scheme { default $scheme; }
# ... existing config ...
}
keys_zone=WORDPRESS:200mstores cache keys in shared memory. 200m is reasonable for a busy single site or a few small sites.inactive=60mdrops entries not used for an hour.max_size=5gprevents the cache from eating your disk.
On multi-tenant boxes, tune this to match your disk size and site count.
If you host many WordPress sites, you’ll usually want separate zones per group or a stricter global limit.
Step 4: Add WordPress-specific bypass rules (the part that prevents breakage)
Microcache only works if you avoid caching the wrong requests.
If you cache sessions or preview pages, failures are predictable. You’ll see stuck carts, cross-user content, and admin screens showing old data.
Create a file for maps (cleaner than stuffing everything into a vhost):
sudo nano /etc/nginx/conf.d/wordpress-cache-maps.conf
Paste:
# Bypass cache for logged-in users and common e-commerce/session cookies
map $http_cookie $skip_cache_cookie {
default 0;
~*"wordpress_logged_in" 1;
~*"wp-postpass" 1;
~*"comment_author" 1;
~*"woocommerce_items_in_cart" 1;
~*"woocommerce_cart_hash" 1;
~*"wp_woocommerce_session" 1;
}
# Bypass cache for admin and dynamic endpoints
map $request_uri $skip_cache_uri {
default 0;
~*^/wp-admin/ 1;
~*^/wp-login\.php 1;
~*^/wp-json/ 1;
~*^/xmlrpc\.php 1;
~*^/wp-cron\.php 1;
~*^/\?s= 1;
~*^/cart/ 1;
~*^/checkout/ 1;
~*^/my-account/ 1;
~*^/\?add-to-cart= 1;
}
# Combine signals
map "$skip_cache_cookie$skip_cache_uri" $skip_cache {
default 0;
~*1 1;
}
This is intentionally explicit. It’s easy to reason about in production.
It’s also simple to extend when a membership or LMS plugin adds new session endpoints.
Step 5: Wire microcache into your site’s Nginx server block
Now apply caching where PHP runs.
Open your WordPress vhost, usually:
sudo nano /etc/nginx/sites-available/example.com
In your server {} block, find the PHP location.
It’s commonly location ~ \.php$ or the WordPress front-controller setup.
A typical WordPress+PHP-FPM block looks like this:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Replace it with a caching-aware block (adjust PHP socket for your version):
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
# Microcache (only safe for anonymous traffic due to $skip_cache)
fastcgi_cache WORDPRESS;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# Cache key should include scheme, method, host, and URI
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# TTLs: short on purpose
fastcgi_cache_valid 200 301 302 10s;
fastcgi_cache_valid 404 1s;
# Prevent thundering herd under bursts
fastcgi_cache_lock on;
fastcgi_cache_lock_timeout 10s;
# Serve stale on upstream hiccups (keeps site responsive)
fastcgi_cache_use_stale error timeout updating http_500 http_503;
# Visibility
add_header X-Cache $upstream_cache_status always;
}
Keep TTL short. Microcache isn’t “cache the blog for a day.”
It’s “stop rebuilding the same page 200 times in a minute.” Ten seconds is often enough to cut PHP work by an order of magnitude during bursts.
Also confirm your WordPress front controller remains standard:
location / {
try_files $uri $uri/ /index.php?$args;
}
Step 6: Add a restricted purge endpoint (without exposing a public purge)
You need a safe way to clear cache after publishing, menu updates, or template changes.
Deleting the entire cache works, but it’s heavy-handed.
A small, IP-restricted purge endpoint gives you control without advertising “purge everything” to the internet.
Add this inside the same server {} block:
# Purge endpoint: only allow your admin IP(s)
location ~* ^/purge(/.*) {
allow 203.0.113.10; # replace with your IP
allow 127.0.0.1;
deny all;
fastcgi_cache_purge WORDPRESS "$schemeGET$host$1";
}
Important: fastcgi_cache_purge requires the ngx_cache_purge module. It isn’t built into all Nginx packages.
If your Nginx doesn’t support it, use the “manual purge” method below.
Manual purge (works everywhere)
If the purge module isn’t available, fall back to a controlled cache wipe.
This clears cache for all sites sharing the path, so be careful on multi-tenant servers.
sudo systemctl stop nginx
sudo rm -rf /var/cache/nginx/fastcgi/*
sudo systemctl start nginx
A less disruptive alternative is to move the directory aside and recreate it.
This is often faster on large caches:
sudo systemctl stop nginx
sudo mv /var/cache/nginx/fastcgi /var/cache/nginx/fastcgi.old.$(date +%F-%H%M)
sudo mkdir -p /var/cache/nginx/fastcgi
sudo chown -R www-data:www-data /var/cache/nginx/fastcgi
sudo chmod 700 /var/cache/nginx/fastcgi
sudo systemctl start nginx
Step 7: Test config safely, reload, then verify cache HIT/BYPASS
Test syntax first:
sudo nginx -t
Reload:
sudo systemctl reload nginx
Then verify headers:
curl -I https://example.com/ | grep -i x-cache
curl -I https://example.com/ | grep -i x-cache
Expected pattern:
- First request:
X-Cache: MISS - Second request (within TTL):
X-Cache: HIT
Check bypass behavior by sending a fake logged-in cookie:
curl -I -H 'Cookie: wordpress_logged_in=1' https://example.com/ | grep -i x-cache
You should see BYPASS or MISS, not HIT.
Practical tuning: TTLs, exclusions, and WooCommerce reality
Microcache shines on content-heavy sites with lots of anonymous traffic.
WooCommerce can still benefit, but only if you’re strict about what bypasses.
- Start conservative: 10s TTL for
200/301/302. If behavior stays correct, try 30s. - Exclude anything personalized: cart, checkout, account, logged-in, previews.
- Don’t cache API-heavy pages: if your theme hits
/wp-json/frequently, keep it bypassed (already done above).
If you also run a plugin page cache, choose one page-caching layer.
Two full-page caches usually turn into “why is this page old?” debugging sessions.
If you must keep a plugin cache, keep the Nginx microcache TTL low and rely on $skip_cache rules.
Quick diagnostics when something looks wrong
Most microcache issues show up immediately in headers and logs.
Use this checklist to narrow it down quickly.
-
You never see HIT
- Confirm requests are reaching the PHP block you added caching to.
- Check cache directory permissions:
sudo -u www-data touch /var/cache/nginx/fastcgi/test - Make sure
$skip_cacheisn’t always 1 due to an overly broad map.
-
Logged-in users see cached pages
- Check cookie names. Some plugins set their own auth/session cookies.
- Add extra cookie matches to
wordpress-cache-maps.conf.
-
Admin changes don’t show up for a minute
- That’s TTL doing its job. Lower TTL to 10s, or purge after publishing.
- Confirm you’re not caching
/wp-admin/or/wp-json/.
-
502/504 errors appear under load
- Microcache reduces PHP work, but upstream timeouts still matter. Use your existing troubleshooting flow: fix 502/504 on Nginx + PHP-FPM.
Make it measurable: log cache status and top cache-bypassed URLs
Headers are great for spot checks.
Logs are what you use to tune and confirm trends.
Edit /etc/nginx/nginx.conf and define a log format inside http {} (or extend your existing one):
log_format main_ext '$remote_addr - $host "$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'cache=$upstream_cache_status ref="$http_referer" ua="$http_user_agent"';
Then apply it in your server or globally:
access_log /var/log/nginx/access.log main_ext;
Reload Nginx and scan a sample:
sudo systemctl reload nginx
sudo tail -n 200 /var/log/nginx/access.log | grep 'cache='
If you see steady cache=BYPASS on the homepage for anonymous users, your map rules are catching more than you intended.
Related hosting tasks you’ll usually do next
Microcache is one layer.
Most fast, stable WordPress stacks rely on a few boring improvements that don’t conflict with each other.
- Terminate and automate HTTPS properly (and fix renewals fast): Let’s Encrypt deployment and troubleshooting.
- Harden the edge without breaking WordPress: Nginx security headers.
- Keep backups boring and test restores (don’t skip restore tests): VPS snapshot backups with restore tests.
Common pitfalls (so you don’t learn them in production)
- Caching POST requests: don’t. This tutorial only caches normal GET responses.
- Caching by IP instead of cookies: cookie-based bypass matches WordPress behavior. IP-based logic breaks for NAT users and mobile networks.
- Using long TTLs without purge: if you want minutes/hours, implement a solid purge workflow first. Microcache is safer.
- Ignoring disk alerts: if you set
max_sizetoo high on a small VPS, the cache can fill disk and take the site down.
Summary: a small cache that makes your VPS feel bigger
This Nginx caching tutorial set up a controlled microcache for WordPress. It uses short TTLs, strict bypass rules, and headers/logs you can trust.
Done well, it cuts PHP-FPM work sharply during bursts. That’s exactly when limited CPU starts to hurt.
If you plan to roll this out across multiple sites, or you want help picking the right CPU/RAM/NVMe profile, start with a HostMyCode VPS. If you’d rather not babysit patch cycles and tuning, managed VPS hosting is the practical option.
If your WordPress site slows down during bursts, microcaching is one of the quickest fixes you can deploy. Use a HostMyCode VPS if you want full control, or choose managed VPS hosting if you want the stack patched, tuned, and maintained for you.
FAQ
Will Nginx microcache break WordPress admin or logged-in users?
Not if you bypass on WordPress login cookies and admin URLs.
Keep wordpress_logged_in and /wp-admin/ excluded, then validate behavior with X-Cache headers.
What TTL should I use for microcaching?
Start at 10 seconds for 200/301/302. If freshness looks good, try 30 seconds. Avoid long TTLs unless you have a reliable purge workflow.
Do I still need a WordPress caching plugin?
Often, no. Nginx microcache handles anonymous full-page caching at the server edge.
If you keep a plugin for other features (minify, preload, etc.), make sure you’re not running two competing full-page caches.
How can I confirm caching is actually reducing PHP load?
Look for X-Cache: HIT on repeated requests, then watch PHP-FPM activity during a burst.
Logging $upstream_cache_status makes the impact easy to quantify.
What’s the safest way to purge cache after publishing?
Use a restricted purge endpoint limited to your IP, or do a controlled directory wipe during low traffic. Keep TTL short so purging rarely becomes urgent.