In the previous chapter, I walked you through the process of configuring Nginx to serve your WordPress sites over HTTPS. However, we need to do more if we want our sites to feel snappy. In this chapter I will guide you through the process of caching a WordPress based site. Caching will increase throughput (requests per second) and decrease response times (improve load times).

Initial Benchmarks: How Bad is WordPress Performance Without Caching?

I want to show you how this setup handles traffic prior to any caching. It’s difficult to simulate real web traffic, however it is possible to send a large amount of concurrent requests to a server and track the time of responses. This gives you a rough indication of the amount of traffic a server can handle, but also allows you to measure the performance gains once you’ve implemented the optimizations.

The server I have setup for this series is a 1GB DigitalOcean Droplet. I’m using Loader to send an incremental amount of concurrent users to the server within a 60 second time period. The users scale, starting with 1 concurrent user and increasing to 50 concurrent users by the end of the test.

Initial benchmark results

The server was able to handle a total of 1,322 requests. You’ll see that as concurrent users increase so does the site’s response time. Meaning the more visitors on the site, the slower it will load. Based on the results, the server can theoretically handle 1,903,680 requests a day with an average response time of 1,134ms.

Monitoring the server’s resource usage shows that the load is split between both PHP and MySQL.

htop results

It’s time to optimize!

Object Cache

An object cache stores database query results so that instead of running the query again the next time the results are needed, the results are served from the cache. This greatly improves the performance of WordPress as there is no longer a need to query the database for every piece of data required to return a response.

Redis is the latest and greatest when it comes to object caching. However, popular alternatives include Memcache and Memcached.

To install Redis, issue the following commands.

sudo apt install redis-server
sudo service php7.4-fpm restart

In order for WordPress to use Redis as an object cache, you need to install a Redis object cache plugin. Redis Object Cache by Till Krüss is a good choice.

Object Cache - Plugins Screen

Once installed and activated, go to Tools > Redis to enable the object cache.

Object Cache - Enable

This is also the screen where you can flush the cache if required.

Object Cache - Flush

I’m not going to run the benchmarks again as the results won’t dramatically change. Although object caching reduces the average amount of database queries on the front page from 22 to 2 (theme and plugin dependant), the database server is still being hit. Establishing a MySQL connection on every page request is one of the biggest bottlenecks within WordPress.

The benefit of object caching can be seen when you look at the average database query time, which has decreased from 2.1ms to 0.3ms. The average query times were measured using Query Monitor.

In order to see a big leap in performance and a big decrease in server resource usage, we must avoid a MySQL connection and PHP execution altogether.

Page Cache

Although an object cache can go a long way to improving your WordPress site’s performance, there is still a lot of unnecessary overhead in serving a page request. For many sites, content is rarely updated. It’s therefore inefficient to load WordPress, query the database and build the desired page on every single request. Instead, you should serve a static HTML version of the requested page.

Nginx allows you to automatically cache a static HTML version of a page using the FastCGI module. Any subsequent requests to the page will receive the cached HTML version without ever hitting PHP or MySQL.

Setup requires a few changes to your Nginx server block. If you would find it easier to see the whole thing at once, feel free to download the complete Nginx config kit now. Otherwise, open your virtual host file:

sudo nano /etc/nginx/sites-available/ashleyrich.com

Add the following line before the server block, ensuring that you change the fastcgi_cache_path directive and keys_zone. You’ll notice that I store my cache within the site’s directory, on the same level as the logs and public directories.

fastcgi_cache_path /home/ashley/ashleyrich.com/cache levels=1:2 keys_zone=ashleyrich.com:100m inactive=60m;

You need to instruct Nginx to not cache certain pages. The following will ensure admin screens and pages for logged in users are not cached, plus a few others. This should go above the first location block.

set $skip_cache 0;

# POST requests and urls with a query string should always go to PHP
if ($request_method = POST) {
    set $skip_cache 1;
}   
if ($query_string != "") {
    set $skip_cache 1;
}   

# Don’t cache uris containing the following segments
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
    set $skip_cache 1;
}   

# Don’t use the cache for logged in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
    set $skip_cache 1;
}

Next, within the PHP location block add the following directives.

fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache ashleyrich.com;
fastcgi_cache_valid 60m;

Download the complete set of Nginx config files

Notice how the fastcgi_cache directive matches the keys_zone set before the server block. In addition to changing the cache location, you can also specify the cache duration by replacing 60m with the desired duration in minutes. The default of 60 minutes is a good starting point for most people.

If you modify the cache duration, you should consider updating the inactive parameter in the fastcgi_cache_path line as well. The inactive parameter specifies the length of time cached data is allowed to live in the cache without being accessed before it is removed.

Once happy, save the configuration.

Next you need to add the following directives to your nginx.conf file.

sudo nano /etc/nginx/nginx.conf

Add the following below the Gzip settings.

##
# Cache Settings
##

fastcgi_cache_key "$scheme$request_method$host$request_uri";
add_header Fastcgi-Cache $upstream_cache_status;

The first directive instructs the FastCGI module on how to generate key names and the second adds an extra header to server responses so that you can easily determine whether a request is being served from the cache.

Save the configuration and restart Nginx.

sudo service nginx restart

Now when you visit the site and view the headers, you should see an extra parameter.

Nginx - Response Headers

The possible return values are:

  • HIT – Page cached
  • MISS – Page not cached (refreshing should cause a HIT)
  • BYPASS – Page cached but not served (admin screens or when logged in)

The final step is to install the Nginx Cache plugin also by Till Krüss. This will automatically purge the FastCGI cache of specific cache files whenever specific WordPress content changes. You can also manually purge the entire cache from the top bar in the WordPress dashboard.

You can also purge the entire cache by SSH’ing into your server and removing all the files in the cache folder:

sudo rm -Rf /home/ashley/ashleyrich.com/cache/*

Especially handy when your WordPress dashboard is inaccessible, like a redirect loop that has been cached.

Once installed, navigate to Tools > Nginx and define your cache zone path. This should match the value you specified in your Nginx hosts file.

WooCommerce FastCGI Cache Rules

Although page caching is desired for the majority of front-end pages there are times when it can cause issues, particularly on eCommerce sites. For example, in most cases you shouldn’t cache the shopping cart, checkout or account pages as they are generally unique for each visitor (you wouldn’t want visitors seeing the contents of other visitor’s shopping carts).

Additional cache exclusions can be added using conditionals and regex expressions. The following example will work for the default pages (Cart, Checkout and My Account) created by WooCommerce :

if ($request_uri ~* "/(cart|checkout|my-account)/*$") {
    set $skip_cache 1;
}

Open the configuration file for your chosen site, in my case:

sudo nano /etc/nginx/sites-available/ashleyrich.com

Add the new exclusion to the server directive, directly below the existing conditionals. Once you’re happy, save, test and reload the configuration for the changes to take effect. You should now see that the ‘fastcgi-cache’ response header it set to ‘BYPASS’ when visiting any of the WooCommerce pages.

WooCommerce isn’t the only plugin to create pages that you should exclude from the FastCGI cache. Plugins such as Easy Digital Downloads, WP eCommerce, BuddyPress and bbPress all create pages that you will need to exclude. Each plugin should have documentation on how to add caching rules to exclude its pages from caching.

Final Benchmarks: How Much Better is WordPress Performance With Caching?

With the caching now configured it’s time to perform a final benchmark. This time I’m going to up the maximum concurrent users from 50 to 750.

Final benchmark results

Not bad at all! The server was able to handle a total of 222,323 requests with an average response time of 101ms. You’ll notice that the response time doesn’t increase at the same rate as the number of concurrent users.

The server’s resource usage looks a little different too. Nginx is now solely causing the spike in CPU.

Final htop results

Performance optimization is a lot more difficult on highly dynamic sites where the content updates frequently, such as those that use bbPress or BuddyPress.

In these situations it’s required to disable page caching on the dynamic sections of the site (the forums for example). This is achieved by adding additional rules to the skip cache section within the Nginx server block. This will force those requests to always hit PHP and generate the page on the fly. Doing so will often mean you have to scale hardware sooner, thus increasing server costs. Another option is to implement micro caching.

Caching Plugins

At this point you may be wondering why I chose this route instead of installing a plugin such as WP Rocket, W3 Total Cache or WP Super Cache. Firstly, not all plugins include an object cache and for those that do you will often need to install additional software on the server (Redis for example) in order to take full advantage of the feature. Secondly, caching plugins don’t perform as well as server-based caching.

That concludes this chapter. In the next chapter we’ll dig into cron, email and automated backups.


Subscribe to get the latest news, updates and optimizations in performance and security.

Thanks for subscribing 👍

To receive awesome stuff, you'll need to head to your inbox and click on the verification link we sent you.
Make sure to check your "spam" folder or your "promotions" tab (if you have Gmail).
If you're still having trouble, then messages us at sudo@spinupwp.com.