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.

We tested every step in this guide on Ubuntu 24.04 (Nginx 1.24, PHP 8.3) and Debian 13 (Nginx 1.26, PHP 8.4) with WordPress 7.1, each in a fresh container. The outputs below are copied from those tests.

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.com in /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/wordpress is the folder for the saved pages.
  • keys_zone=wordpress:10m gives 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_key decides 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 wordpress uses the cache from step 1.
  • fastcgi_cache_valid: normal pages and redirects are kept for 60 minutes. Error pages are not saved.
  • fastcgi_cache_bypass and fastcgi_no_cache: for the pages from step 2, don't hand out a saved copy and don't save a new one.
  • X-Cache adds 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.

This only works if PHP runs as the same user as Nginx (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-Cache header at all: the reload didn't happen, or you edited a different server block. Run sudo nginx -T | grep fastcgi_cache to 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 with curl.
  • 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 with curl -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_headers to "fix" a MISS; 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_path and fastcgi_cache_key in /etc/nginx/conf.d/fastcgi-cache.conf, and sudo mkdir -p /var/cache/nginx.
  • Skip rules in the server block: 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

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.

← Back to the security blog