Caching is one of the easiest ways to improve the performance of a WordPress website, but it can also become unnecessarily complicated.
You install a caching plugin, enable a few settings, and suddenly everything is supposed to be faster.
The problem is that not all caches do the same thing.
On the WordPress servers I’ve worked with, there are four main caching layers: the browser cache, Cloudflare CDN cache, Nginx proxy cache and Redis object cache.
Each one works at a different point in the request, and understanding what they do makes it much easier to work out where a performance problem is coming from.
Browser caching
The first layer is the visitor’s browser.
When somebody opens a website, the browser downloads resources such as CSS, JavaScript, fonts and images.
Most of those resources don’t need to be downloaded again every time somebody visits another page.
HTTP caching headers tell the browser whether a response can be reused and for how long.
For example:
1Cache-Control: public, max-age=31536000, immutable
This tells the browser that the resource can be cached for a year and that it isn’t expected to change.
It’s particularly useful for assets with versioned or hashed filenames, where a new deployment produces a different URL.
You wouldn’t generally want to apply that policy to ordinary HTML pages, because visitors could end up seeing old content long after it’s been updated.
Cloudflare CDN caching
The next layer is Cloudflare.
Cloudflare sits between the visitor and the origin server, and can cache eligible responses at its edge locations.
If an image is already cached at the edge, Cloudflare can return it without requesting it from the WordPress server again.
That reduces the number of requests reaching the server and can improve performance for visitors who are geographically distant from it.
By default, Cloudflare generally caches eligible static assets rather than all HTML pages.
You can configure it to cache HTML too, but that needs more care.
WordPress pages can contain user-specific content, especially for logged-in users, shopping baskets and checkout pages.
Caching the wrong HTML response and serving it to somebody else would be considerably worse than having a slightly slow website.
For that reason, cache rules and bypass conditions matter just as much as the cache itself.
Nginx proxy caching
The third layer is Nginx, running on the origin server.
With EasyEngine, Nginx can cache generated page responses so subsequent requests don’t need to run WordPress again.
For example, imagine a visitor requests an uncached blog post.
Nginx forwards the request to PHP, WordPress runs, queries the database and generates the HTML.
Nginx can then store that response.
The next visitor requesting the same page may receive the cached HTML directly from Nginx, without PHP needing to do anything.
This is particularly useful for pages that receive a lot of traffic but don’t change frequently.
It also reduces the amount of work PHP-FPM has to do, leaving more capacity for requests that can’t be cached.
Redis object caching
Redis works differently from the other three layers.
Rather than caching the final HTML response, Redis stores objects that WordPress uses while generating the page.
By default, WordPress has an object cache which only lasts for the duration of a request.
With a persistent object cache backed by Redis, cached data can survive between requests.
This avoids repeatedly fetching the same information from the database.
For example, WordPress might need to retrieve site options, post metadata or other frequently accessed information.
If that data is already available in Redis, WordPress can use it without repeating the database query.
Redis is particularly useful when the request cannot be served from a full-page cache, such as when a user is logged in or the page contains dynamic content.
I’ve written a separate guide on setting up Redis object caching in WordPress, including how to configure it with EasyEngine and verify that WordPress is actually using the cache.
What happens when somebody visits a page?
With all four layers enabled, a request might look something like this:
1Visitor 2 | 3 v 4Browser cache 5 | 6 | Cache miss 7 v 8Cloudflare CDN 9 |10 | Cache miss11 v12Nginx proxy cache13 |14 | Cache miss15 v16PHP / WordPress17 |18 v19Redis object cache20 |21 | Object cache miss22 v23Database
Not every request travels all the way through the stack.
A cached image might never reach Cloudflare, because the browser already has it.
A cached HTML page might be returned by Cloudflare or Nginx, without WordPress executing.
Redis only becomes relevant once PHP is actually running WordPress and needs to retrieve cached data.
This is why adding Redis to a website with an already effective full-page cache might not produce a dramatic improvement in public page load times.
The difficult part is clearing the cache
Having several caching layers is useful until you update something and the changes don’t appear.
You might clear the WordPress object cache, but Nginx is still serving an old HTML page.
You clear Nginx, but Cloudflare has its own cached copy.
And even after purging Cloudflare, the visitor’s browser might still be using a previously cached resource.
This is why cache invalidation needs to be considered when configuring the stack.
I try to make sure that publishing or updating content clears the appropriate page caches automatically, rather than relying on manually purging everything.
For static assets, versioned filenames make this much easier because the URL changes whenever the file changes.
I’d also avoid setting long cache lifetimes without having a way to invalidate them.
More caching isn’t necessarily better
It’s tempting to enable every caching option available, but each additional layer introduces another place where something can go wrong.
A badly configured cache can serve stale content, interfere with dynamic pages or make debugging considerably harder.
There’s also a difference between reducing server load and improving the experience of the visitor.
If Cloudflare is already returning a page from the edge, speeding up the WordPress database won’t make that particular response much faster.
I find it more useful to understand where requests are spending their time and optimise the layer that’s actually doing the work.
Browser caching reduces repeated downloads. Cloudflare reduces requests to the origin. Nginx reduces PHP execution. Redis reduces repeated database work within WordPress.
They complement each other, but they aren’t interchangeable.
Once you understand that distinction, WordPress caching becomes a lot less mysterious.