WordPress nginx page cache: set up FastCGI cache the right way
Set up a WordPress nginx page cache with FastCGI cache: full config, bypass rules for logins and carts, purging on updates and HIT/MISS checks.

Key takeaways
- An nginx page cache stores the finished HTML of a WordPress page and serves it to the next visitor without running PHP or the database.
- Logged-in users, carts, checkout, POST requests and URLs with query strings must always bypass the cache.
- You need a way to purge the cache when content changes, or visitors see stale posts.
- In our measurements, page cache raised a 4 GB VPS from about 100 to 250 WordPress sites.
A WordPress nginx page cache uses nginx's FastCGI cache to store the HTML that WordPress generates and hand that copy to the following visitors, without starting PHP or querying MySQL/MariaDB. It is the most effective server-side speed-up for WordPress: cached pages come back in milliseconds instead of hundreds of milliseconds, and the same VPS can host far more sites. Doing it right means excluding logged-in users, carts and checkout, and purging the cache when content changes.
This guide explains how the page cache works, gives a complete nginx configuration for Ubuntu 24.04 or Debian 12 with the reason for each line, shows how to purge and how to prove the cache is actually working, and how to turn it on in ZoPanel.
How does an nginx page cache work?
Without a cache, every WordPress page view runs the whole chain: PHP boots WordPress, loads plugins and the theme, sends dozens of database queries and builds the HTML. That costs CPU and memory, and it is repeated identically for every visitor reading the same post.
With FastCGI cache, the first request still goes through PHP (a MISS), but nginx stores the result on disk. Later requests for the same URL are served by nginx straight from the cache (a HIT); PHP and the database do nothing. When a cached copy expires, nginx can serve the old copy immediately while refreshing it in the background, so hundreds of visitors hitting a just-expired page do not all trigger PHP at once.
Page cache vs object cache, browser cache and CDN
| Cache | What it stores | Where | Most useful for |
|---|---|---|---|
| Page cache (nginx FastCGI) | Whole HTML pages | On the server, in front of PHP | Anonymous visitors, blogs, news, landing pages |
| Object cache (Redis) | Database query results | In RAM, used by PHP | wp-admin, WooCommerce, logged-in users |
| OPcache | Compiled PHP code | RAM/disk | Every request that runs PHP |
| Browser cache | Images, CSS, JS | The visitor's device | Second and later page views |
| CDN | Static files, sometimes HTML | Edge servers near visitors | Distant visitors, bandwidth |
The layers complement each other. Page cache gives the biggest win for anonymous traffic; object cache helps the pages that can never be cached whole.
How much does page cache help?
We measured it on real servers: an Ubuntu 24.04 VPS with 4 GB of RAM and 4 vCPUs hosted about 100 WordPress sites without page cache and 250 with it, alongside 30 other applications. On a 3 GB Debian 12 VPS the numbers were 50 and 200. The method, traffic mix and limits of those numbers are in how many WordPress sites a 4 GB VPS can host. Your sites will differ with plugins, traffic and the share of logged-in visitors.
How to set up a WordPress nginx page cache by hand
This assumes WordPress already runs on nginx with PHP-FPM (for example PHP 8.3 on the socket /run/php/php8.3-fpm.sock). Back up your nginx configuration before editing.
Step 1: Define the cache zone
Create /etc/nginx/conf.d/wordpress-cache.conf (files in conf.d are included in the http block):
fastcgi_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=WORDPRESS:64m
inactive=1d max_size=2g use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
keys_zone=WORDPRESS:64mis the shared memory for cache keys; one megabyte holds about eight thousand keys.inactive=1ddrops pages nobody has requested for a day.max_size=2gcaps the cache on disk.- The key includes
$host, so several sites can share one zone without mixing up their pages.
Step 2: Bypass rules
In the site's server block, before the location blocks:
set $skip_cache 0;
# Cache only GET and HEAD
if ($request_method !~ ^(GET|HEAD)$) { set $skip_cache 1; }
# URLs with a query string (search, filters, ?page= pagination)
if ($query_string != "") { set $skip_cache 1; }
# Admin, login, API, feeds, sitemaps, cart, checkout, account
if ($request_uri ~* "/wp-admin/|/wp-login\.php|/wp-json/|/xmlrpc\.php|/feed/|sitemap|/cart/|/checkout/|/my-account/") {
set $skip_cache 1;
}
# Logged-in users, commenters, password-protected posts, WooCommerce carts
if ($http_cookie ~* "wordpress_logged_in|wp-postpass|comment_author|woocommerce_items_in_cart|wp_woocommerce_session") {
set $skip_cache 1;
}
Using if only to set a variable, as here, is a safe use of if in nginx. If your shop uses different cart or checkout paths (translated slugs, for example), add them to the list.
Step 3: Turn on the cache for PHP
In your existing location ~ \.php$:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 301 10m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_lock on;
add_header X-Cache $upstream_cache_status;
}
fastcgi_cache_valid 200 301 10m: successful pages are kept for 10 minutes. 404s and errors are not cached.fastcgi_cache_bypassandfastcgi_no_cache: excluded requests neither read from nor write to the cache.use_stale updatingwithbackground_update: when a page expires, visitors still get the old copy immediately while one background request refreshes it.error timeout http_500 http_503also serves the old copy while PHP is failing, which carries a site through short outages.fastcgi_cache_lock: many simultaneous requests for an uncached page produce a single PHP request.X-Cache: shows HIT, MISS or BYPASS.
Note that add_header inside a location replaces every add_header from the server level (security headers, for example). If you set headers at server level, repeat them here. The nginx FastCGI module documentation covers every directive.
Create the cache directory, test and reload:
sudo mkdir -p /var/cache/nginx/wordpress
sudo nginx -t && sudo systemctl reload nginx
Step 4: Purge the cache when content changes
Without purging, visitors may see an old version of a post for up to 10 minutes after you edit it. Three options:
- A short cache time (a few minutes), accepting the delay. The simplest.
- The Nginx Helper plugin with the
ngx_cache_purgemodule: the plugin asks nginx to purge the affected pages whenever a post is published or edited or a comment is approved. On Ubuntu 24.04 and Debian 12 the module is packaged aslibnginx-mod-http-cache-purge:
sudo apt install -y libnginx-mod-http-cache-purge
Add a location to the server block that only the server itself may call:
location ~ /purge(/.*) {
allow 127.0.0.1;
allow YOUR_SERVER_IP;
deny all;
fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1";
}
The purge key must match the fastcgi_cache_key from step 1. Then install Nginx Helper, enable purging and choose the ngx_cache_purge method. If your cache directory differs from the plugin's default, declare it in wp-config.php as the plugin's documentation explains.
- Clear everything by hand when you must:
sudo find /var/cache/nginx/wordpress -type f -delete.
Step 5: Prove the cache works
In a private window or with curl (no login cookies), request the same URL twice:
curl -sI https://example.com/ | grep -i x-cache # first: MISS
curl -sI https://example.com/ | grep -i x-cache # second: HIT
If you always see MISS or BYPASS, the usual causes are:
- A plugin sends
Set-Cookieon every page. Per nginx's documentation, responses withSet-Cookieare not cached. Find the plugin (often analytics, multilingual or cart plugins) and reconfigure it. - WordPress or a plugin sends
Cache-Control: no-cacheor anExpiresdate in the past. - The URL has a query string, such as an ad link with
?utm_source=.
Turning on page cache in ZoPanel
ZoPanel includes the nginx page cache for every PHP website, with the rules above built in:
- Open the website and go to the PHP & config tab.
- Turn on Page cache.
What you get, per the Websites and PHP documentation:
- Pages are cached for 10 minutes; an expired page is served at once while one request refreshes it in the background; pages unused for a day leave the cache.
- Never cached: requests other than GET and HEAD; visitors with a WordPress login, password-protected post, comment author, WooCommerce cart or session cookie;
/wp-admin,/wp-login.php,/wp-json,/xmlrpc.php,/cart,/checkout,/my-account,/feedand sitemap URLs; and URLs with a query string. A query made only of ad-tracking parameters such asutm_*,fbclidorgclidcounts as no query, so campaign visitors still get cached pages. - WordPress sites get a small must-use plugin that purges the site's cached pages when posts, comments, menus, the theme or plugins change. Purge cache clears it by hand, and responses carry an
X-Cacheheader. - The Tuning page lists WordPress sites without page cache and has an Apply button to turn it on for all of them.
For WooCommerce and membership sites, also turn on the Redis object cache on the same tab: ZoPanel creates a private Redis database for the site and configures the plugin. See also WordPress Toolkit and PHP performance. Note that page cache is off in Apache mode.
Common WordPress nginx page cache mistakes
- Caching logged-in pages: other visitors see someone's admin bar or account details. Always check the
wordpress_logged_incookie. - Caching cart and checkout: visitors see someone else's cart. Exclude your shop's actual URLs, including renamed ones.
- No purge mechanism: visitors see old prices or posts.
- Stacking a PHP page-cache plugin on top of nginx's cache: two caches of the same kind are hard to purge together and hard to debug. Pick one.
- Different mobile and desktop themes sharing one cache key: mobile visitors get the desktop page. Responsive themes do not have this problem.
- Never checking
X-Cacheafter setup, and assuming a cache that is actually bypassed on every page.
Conclusion
A WordPress nginx page cache is the single change with the biggest effect on speed and density on a VPS: faster pages, far less PHP and database work, and better resilience to traffic spikes. The hard part is getting it right: complete bypass rules, purging when content changes, and proving it works. If you want that done for every site, try ZoPanel's Free plan with 10 websites and turn page cache on with one switch.
Frequently asked questions
Do I need a plugin for an nginx page cache?
Not for the cache itself: FastCGI cache runs entirely in nginx. A plugin is only needed to purge automatically when content changes, for example Nginx Helper with the ngx_cache_purge module; otherwise use a short cache time or purge by hand. ZoPanel installs its own small must-use plugin for purging.
Can I use page cache with WooCommerce?
Yes, for product and category pages viewed by visitors with an empty cart. Cart, checkout, account pages and any visitor with items in the cart must bypass it. Shops with many logged-in customers should add a Redis object cache.
How long should pages stay cached?
With purge-on-update in place, 10 minutes to a few hours works well, combined with background refresh. Without purging, keep it to a few minutes so new content is not delayed for long.
How can I tell whether a page was served from cache?
Check the X-Cache response header with curl -sI https://example.com/ | grep -i x-cache while logged out. The first request shows MISS and the next one HIT. BYPASS means a rule excluded that request.
How is nginx page cache different from WP Super Cache or W3 Total Cache?
Plugin caches either run through PHP or rely on rewrite rules to serve stored HTML files, while FastCGI cache is served by nginx before PHP is involved, which is lighter at the server level. Do not run two page caches at once; the plugin's other features (image compression, CSS combining) can still be used alongside.
Install ZoPanel free
One command on a fresh Ubuntu or Debian server. Up and running in minutes.
Install in 5 minutesKeep reading

Best VPS specs for WordPress: how much CPU, RAM and disk you need
The best VPS specs for WordPress for 1, 10 or 100 sites: how to size CPU, RAM and disk from your traffic, with real measurements and the mistakes to avoid.

How to replace WP-Cron with a real cron job (and why you should)
Replace WP-Cron with real cron to stop missed scheduled posts and late WooCommerce emails. Step-by-step crontab, WP-CLI and systemd timer setups that work.

How to speed up WordPress: 10 steps in order of impact
Speed up WordPress in the order that matters - measure, page cache, PHP 8.3, Redis, database, images, plugins and cron - with test commands and the ZoPanel way.