When I first started hosting my own sites, I had no idea what caching was or why it was important. Then I wrote a couple of popular blog posts, and my server crashed.
Fast forward a few years, and I’m running a few different websites on a few different servers. Some get a steady stream of traffic; others get a huge spike now and again when a post is picked up by Hacker News. In both cases, configuring your cache is a great way to keep things from failing.
Thanks to a clever configuration of Nginx and Redis, WordPress has achieved … ludicrous speed!
WordPress is my application of choice for just about everything. It’s easy to set up, easy to extend, and best of all, it’s open source.
You’ll notice one thing I really care about is the fact that open source exists and helps train a new generation of software monkeys on how to build the Internet. Fact is, if WordPress were closed source, I would not have the job I do today.
WordPress is built on top of a whole slew of technologies. The data is stored in MySQL, and PHP is used to script both the retrieval of that data and its shoehorning into an HTML template. Unfortunately, this many moving parts means requests can be somewhat slow.
For comparison to the 3 ms reference above, my site takes about 4 (four) seconds to render when it’s not cached.
That is a huge difference between the cached and non-cached performance – 3 ms versus 4000 ms. WordPress is fast, but dynamic page generation is only as fast as the server powering it. Even on the most streamlined machine, rendering a page dynamically will take time.
Nginx is an open-source, event-driven web server. I use it on all of my servers because it’s incredibly more efficient than Apache for handling multiple connections. It’s fast, doesn’t use too much memory, and can be used to proxy requests to PHP or even other handlers like Node and Ruby.
Since we’re working with WordPress, we’re also working with PHP. For reference, a pretty stock Nginx configuration file looks something like this:
Redis is an open source, in-memory, key-value database server – meaning it’s an optimized hashtable for your system. If you’ve ever heard of Memcached, Redis is similar. You can store anything you want – in memory – and retrieve it crazy fast.
Unlike Memcached, though, Redis is fairly easy to install and configure. 2
First things first, you’ll need to install Redis. There are some stock instructions available on the Redis website. There are also some handy, albeit slightly outdated, tutorials lying around the Internet. I used the Centos 6 tutorial as a base for my configuration, even though I run Ubuntu on my server.
Feel free to use whatever installation instructions, tools, or configuration you want (Debian users can just run apt-get install redis-server), just get Redis installed.
To let PHP (and thus, WordPress) interact with Redis, we use a library called Predis. You can grab the entire library if you want, or you can use a really clean all-in-one-file version. This version is the predis.php that will be referenced in the cache file in the next section:
In a nutshell, I put a single PHP script in front of WordPress on my server. This script interfaces with Redis directly to handle the page cache:
- If this isn’t a postback (form submission), the visitor isn’t logged in (based on the presence of a WordPress cookie), and Redis has an existing cached copy of this URL request, return the cache
- If this is a postback (form submission) or the request url is postfixed with ?r=y, flush the cache of the page and load WordPress
- If the visitor is logged in (based on the presence of a WordPress cookie) and the url is postfixed with ?c=y, flush the cache for the entire site and load WordPress
- If the visitor is logged in, load WordPress
The script we load in front of WordPress is fairly simple. It checks the above criteria and either pulls data from Redis or loads WordPress:
Since we’re running this script before loading WordPress we skip all of the overhead associated with firing up MySQL and loading up the entire WordPress environment. Instead, we return whatever HTML we rendered beforehand and respond to new visitor requests faster than without a cache.
But, if certain requirements are met, we still load WordPress like normal.
The biggest trick here is to get our script loaded before WordPress without hacking any core WordPress files. Thanks to Nginx, we can set up an alias for the regular index.php file. This modified Nginx configuration file will load wp-index-redis.php instead of index.php whenever a visitor requests it. Also, this configuration will rewrite WordPress requests through wp-index-redis.php so rewritten page/post/queries will pass through the cache as well:
The one thing that needs to skip the cache, however, is the entire /wp-admin directory. If you pay close attention to the configuration file above, you’ll see that we’re allowing admin requests to pass back to the regular index.php file.
When a request hits my site, Nginx passes it directly to a single PHP script. This script checks an in-memory cache to see if we can load a saved version and, if so, loads it. If we need to refresh things, the script passed the request to WordPress and caches the result for future reference.
This is all accomplished through some clever rewriting of Nginx’s configuration such that we don’t need to hack WordPress directly. Now, visitors get lightning-fast responses to requests, and WordPress core can continue to update without any fear of overwriting cache-specific changes.
Check out the source of my Jumping Duck Media website to see just how quickly the cache returns a response.
- Yes, you read that right. Three milliseconds. This is exclusive of the time it takes to perform a DNS lookup or actually deliver the page. But it is proof that the bottleneck is no longer the server itself. ↩
- I have been working with professionally on the web for over 7 years and have not once been able to configure Memcached properly. Redis, on the other hand, I have had 0 issues with. If Redis confuses you, feel free to swap out my advice with Memcached and keep on trucking. ↩