Make WordPress fast with the Nginx FastCGI cache
Every WordPress page is built by PHP and the database, again and again, for every visitor. Nginx can keep a copy of the finished page and hand it out by itself. Pages load much faster, and a traffic spike no longer pushes PHP to its limit. No caching plugin is needed.
Before you start
- Your site already works on Nginx with PHP-FPM. If posts give a 404, fix that first: WordPress permalinks on Nginx.
- The examples use the site
example.comin/var/www/example.com, with its Nginx file in/etc/nginx/sites-available/example.com. Use your own names, and your own PHP version in the socket path. - Turn off other page caches (a caching plugin's "page cache" option) so you don't have two caches fighting over the same pages. An object cache such as Redis is fine to keep.
- Keep a copy of your Nginx file:
sudo cp /etc/nginx/sites-available/example.com ~/example.com.bak.
1. Tell Nginx where to keep the pages
Create the file /etc/nginx/conf.d/fastcgi-cache.conf with these two lines:
fastcgi_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=wordpress:10m max_size=1g inactive=60m use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
In plain words:
/var/cache/nginx/wordpressis the folder for the saved pages.keys_zone=wordpress:10mgives the cache a name,wordpress, and 10 MB of memory for its list of pages. That's enough for tens of thousands of pages.max_size=1g: the folder never grows past 1 GB.inactive=60m: a page nobody asked for in an hour is removed.fastcgi_cache_keydecides what counts as "the same page": the same address on the same site.
Then create the parent folder. Nginx makes the last folder by itself, but not the ones above it:
sudo mkdir -p /var/cache/nginx
In our test on both systems, /var/cache/nginx didn't exist. Without it, the config test fails with:
[emerg] mkdir() "/var/cache/nginx/wordpress" failed (2: No such file or directory)
nginx: configuration file /etc/nginx/nginx.conf test failed
2. Decide which pages must never be cached
A cached page is the same for every visitor. That's fine for a blog post. It's wrong for anything personal: the admin, a logged-in user's view with the admin bar, a shopping cart. Open your site's Nginx file and add these lines inside the server block, above the location blocks:
set $skip_cache 0;
# Forms and searches
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
# Admin, login, feeds and shop pages
if ($request_uri ~* "/wp-admin/|/wp-login.php|/xmlrpc.php|/wp-json/|wp-.*\.php|/feed/|sitemap(_index)?\.xml|/cart/|/checkout/|/my-account/") { set $skip_cache 1; }
# Logged-in users, people who left a comment, and shoppers with a cart
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in|woocommerce_items_in_cart|woocommerce_cart_hash|wp_woocommerce_session") { set $skip_cache 1; }
If your shop's pages have other addresses (for example /basket/ in another language), add them to the third line.
3. Turn the cache on for PHP
In the same file, find your PHP block (location ~ \.php$) and add five lines after fastcgi_pass:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_cache wordpress;
fastcgi_cache_valid 200 301 302 60m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-Cache $upstream_cache_status always;
}
fastcgi_cache wordpressuses the cache from step 1.fastcgi_cache_valid: normal pages and redirects are kept for 60 minutes. Error pages are not saved.fastcgi_cache_bypassandfastcgi_no_cache: for the pages from step 2, don't hand out a saved copy and don't save a new one.X-Cacheadds a header so you can see what happened (step 4).
Test and reload:
sudo nginx -t && sudo systemctl reload nginx
If nginx -t shows an error, nothing changes and the site keeps running as before. Fix the line it names first (see Nginx won't start or reload).
4. Check that it works
Load the same post three times and look at the X-Cache header:
for i in 1 2 3; do curl -s -o /dev/null -D - https://example.com/hello-world/ | grep -i x-cache; done
X-Cache: MISS
X-Cache: HIT
X-Cache: HIT
- MISS: no saved copy yet. PHP built the page and Nginx saved it.
- HIT: Nginx sent the saved copy. PHP and the database did nothing.
- BYPASS: a page from step 2. PHP built it fresh, as before.
In our test, a post took about 47 ms with PHP and 0.3 ms from the cache, measured on the server itself. On a busy site, or a small server, the difference is much bigger.
We also checked the pages that must stay fresh. The login page, /wp-admin/, searches (?s=), feeds, /cart/, and every visit with a logged-in, comment or WooCommerce cookie gave BYPASS. When we logged in as admin, our page had the admin bar; the next visitor without a cookie got a HIT with no admin bar in it. Error pages such as 404 were never saved.
One trap when testing: curl -I asks for the headers only, and Nginx stores that separately from a normal visit. In our test, the first curl -I said MISS even though the page was already cached for browsers. Check twice with the same command.
5. Clear the cache when you change something
Nginx doesn't know when you edit a post. In our test, after we changed a post's title, visitors kept seeing the old title until the saved copy was removed. That can last up to the 60 minutes from step 3. You have two ways to fix this.
By hand. This clears every saved page; they're saved again on the next visit:
sudo find /var/cache/nginx/wordpress -type f -delete
Do this after you change your theme, menus or widgets, since those show on every page.
With the Nginx Helper plugin. It removes a post's saved copy (and the home page) as soon as you update it. Install Nginx Helper, then tell it where the cache is. Add this line to wp-config.php, above /* That's all, stop editing! */:
define( 'RT_WP_NGINX_HELPER_CACHE_PATH', '/var/cache/nginx/wordpress' );
Then go to Settings → Nginx Helper, tick Enable Purge, choose nginx Fastcgi cache and Delete local server cache files, and save. In our test, updating a post removed both saved pages right away, and the next visit showed the new title.
www-data, the default). Nginx's cache files belong to www-data and only it can delete them. If your site has its own PHP-FPM user (see one PHP-FPM pool per site), the plugin quietly fails: in our test, the post was updated but the old copy stayed. Then clear by hand, or use a shorter time in fastcgi_cache_valid, such as 10m.If something goes wrong
- No
X-Cacheheader at all: the reload didn't happen, or you edited a differentserverblock. Runsudo nginx -T | grep fastcgi_cacheto see what Nginx really uses. - Always
BYPASS: you're logged in, or your test address has a?in it. Test in a private window or withcurl. - Always
MISS: the page sends a cookie or a "don't cache" header, so Nginx won't save it. This is a safety feature; a plugin may be setting a cookie for every visitor. See which withcurl -sI https://example.com/hello-world/ | grep -iE 'set-cookie|cache-control'. - Someone saw another person's page: clear the cache right away, then add that page's address or cookie to step 2. Never add
fastcgi_ignore_headersto "fix" aMISS; that turns off the safety check above. - Behind Cloudflare: this works the same. Cloudflare doesn't cache WordPress pages by default, so there's still only one cache to clear.
Quick list
fastcgi_cache_pathandfastcgi_cache_keyin/etc/nginx/conf.d/fastcgi-cache.conf, andsudo mkdir -p /var/cache/nginx.- Skip rules in the
serverblock: POST,?, admin and login, logged-in and cart cookies. - Five lines in the PHP block, then
sudo nginx -t && sudo systemctl reload nginx. - Check: MISS, then HIT. Logged in or admin: BYPASS.
- After edits: Nginx Helper, or
sudo find /var/cache/nginx/wordpress -type f -delete.
Official sources
- Nginx: fastcgi_cache_path, fastcgi_cache_bypass and fastcgi_no_cache
- Nginx: $upstream_cache_status (MISS, HIT, BYPASS)
- WordPress: Nginx
- Nginx Helper plugin
Next steps: make PHP itself faster with OPcache and the right PHP-FPM settings, and slow down bots with rate limiting.
ServerNest is being built to set up a tested Nginx config for each WordPress site. A cache option like this one is planned. Join early access to try it first.