Technote

Servers & infrastructure Practical

Series Profiling WordPress performance Part 6 of 8

Page caching belongs on the server layer

The layer that stores finished HTML only pays off in front of PHP. Stack a plugin cache on top of a server cache and you now have two invalidation timelines.

Every cache so far worked inside PHP. A page cache is different: it stores the finished HTML response whole and hands it to the next visitor as-is, without booting WordPress or opening a database connection.

That is why this layer only pays off in front of PHP. nginx’s FastCGI cache, or a CDN ahead of it, is the right place. A page cache implemented as a plugin answers after PHP has booted and plugins have loaded, so much of what there was to save has already been spent.

The cache layers — higher means answered earlier. Never run two page caches at once

What stacking two of them does

Add a plugin cache where the server already caches pages and the same response now has two copies with different lifetimes. Publish an edit and the plugin clears its copy, while the server keeps serving the old HTML until its own expiry.

The symptom always looks the same: the admin sees the new page, visitors see the old one. Because the administrator is logged in and therefore bypassing the cache, it reads as “it looks fine on my screen”, and hunting for the cause in application code can burn days.

Which layer holds the stale copy is a header question. Have the server cache report its own state and the check takes a second.

curl -sI https://example.com/ | grep -i '^x-cache'
# HIT  -> served by the server cache
# MISS -> the request reached PHP

Spell out the bypass conditions

Most page-cache incidents come from caching something that should never have been cached. Four conditions are non-negotiable.

set $skip 0;
if ( $request_method = POST )                { set $skip 1; }
if ( $http_cookie ~* "wordpress_logged_in" ) { set $skip 1; }
if ( $request_uri ~* "^/wp-admin/|^/wp-json/acme-payments/" ) { set $skip 1; }

fastcgi_cache_bypass $skip;
fastcgi_no_cache     $skip;
add_header X-Cache   $upstream_cache_status always;

The third line — payment and webhook routes — matters most. If a webhook response called by an external server gets cached, the payment state never updates, and you find out only once orders start piling up. The logged-in cookie condition is equally mandatory: omit it and one user’s personalised screen can be served to somebody else.

Start invalidation narrow

Purging everything on every edit erodes the point of having a cache; purging too narrowly leaves stale cards on listing pages. A practical starting point is the edited post, the archives it belongs to, and the home page, widened when you find a case it misses.

One invalidation pass — without the last box, nobody knows whether it purged

Decide the verification too. Right after publishing, request the same URL twice while logged out: the first should carry the new content, the second should be a cache hit. Skip that and you are running invalidation rules nobody has ever seen work.

Server cache configuration and hardening are covered further in the servers and infrastructure archive, and nginx and Redis configuration is within the scope of our optimization program.

Next part

Sometimes every request stays slightly heavy no matter how many caches you stack. Next: autoload options, and how to find the data that rides along on every single request.

More on this topic

All technotes

Servers & infrastructure Advanced

Moving wp-cron to system cron, and why schedules run late

Scheduled posts that publish late are usually a structural problem, not a code one. WordPress cron rides on incoming requests, so on a quiet site nothing runs at…

Developers 8 min read

₩270,000 · Join the program