Caching

Page Caching Page Caching

The crudest form of caching in WordPress is the page cache. This stores the full generated HTML page in a cache. Page caching typically only caches GET requests, as POST (etc) requests would typically change the output as they’re altering data.

As the full generated page is stored, it’s not possible to have any dynamic content on the page, which is why full-page caching is typically only used for unauthenticated users. While it is possible to also store a full-page cache for authenticated users, the cache hit rate would be too low to be effective, as each user would need their own cache. (Variability is available with some full-page caching using Edge Side Includes, but this isn’t a system we typically use.)

WordPress provides the wp-content/advanced-cache.php drop-in to enable full-page caching. We typically use Batcache (specifically, the HM fork). Batcache stores full-page caches in the object cache, ensuring the cache is horizontally-scalable.

Out of the box, Batcache only understands how to handle WordPress’ built-in cookies and special paths (such as xmlrpc.php). Other requests are cached by default. Any custom cookies or special paths that should disable or otherwise alter the cache need to be configured in Batcache.

In addition to storing pages in the object cache, Batcache also generates browser cache headers (such as Last-Modified and Cache-Control). These are only generated as default headers, and can be overridden by sending the headers yourself in your code. For example, to ensure a given page is not cached, you can use the WordPress nocache_headers() helper function.

Batcache is automatically enabled as part of HM Platform. See the developer guide for details on how to customise Batcache.

Batcache is also used on WordPress.com VIP Classic.

WordPress.com VIP Go uses Varnish for full-page caching, which works in a similar way, but is customised differently.

Edge Caching (CDN) Edge Caching (CDN)

Edge caching stores the full-page cache in servers physically distributed around the world. Requests are automatically routed by the Content Delivery Network (CDN) to the server to the end-user (called the “edge location”). This reduces latency, as requests don’t need to travel around the world to the server, but can instead be served locally. (They are served from the nearest “edge” of the network, rather than having to travel to the app server.)

Typically, edge caching is handled for you as part of the page caching solution. This uses the HTTP headers generated by the page cache to determine how and when to cache pages at the edge.

Edge caching also includes static assets, such as CSS and JS files. Typically, these are cached for a very long time (e.g. a year), and the cache is not cleared upon deployment. Any static assets should build versioning into the URLs (either as part of the filename or in the query string) to explicitly bust the cache.

Sites hosted on HM Cloud use AWS CloudFront for edge caching, which has over 110 edge locations around the world.

Sites hosted on WordPress.com VIP use the WordPress.com CDN, which has over 20 edge locations around the world.

Object Caching Object Caching

Object caching is a granular form of caching specific to WordPress. This allows storing arbitrary objects (e.g. strings, arrays, PHP objects) in a cache shared across app servers. These objects are stored persistently, either for a certain amount of time or until they are invalidated.

Items stored in the object cache can be shared across all requests and all pages, allowing a high cache hit rate. Because of the granularity of object caching, it can be used for both authenticated and unauthenticated users. However, functions must be written to use the object cache, whereas page caching can be automatically enabled for an entire site. Additionally, it must be kept in-sync with the underlying database or other source.

The object cache is much typically faster than the database, as it provides a key-value store. This gives constant-time behaviour, which is almost always faster than database queries or external requests. However, the object cache uses an external cache server, so it still incurs a small latency overhead.

Similarly to page caching, WordPress provides a drop-in for object caching at wp-content/object-cache.php. We typically a Memcache backend server with the memcached drop-in. However, this is an implementation detail, and you typically won’t need to worry about this.

“Memcache” and “Memcached” are interchangeable names for the backend cache server. However, the name of the PHP extension used on the app server is Memcached, which is not the same as the legacy Memcache extension.

Interacting with the object cache is typically done through the WordPress object cache API. The wp_cache_*() family of functions should be always used instead of accessing the underlying memcache implementation.

Out of the box, WordPress automatically uses the object cache inside most functions that access the database. Custom functions should implement their own caching atop the object cache API.

Sites hosted on HM Cloud use Amazon ElastiCache for Memcached as the object caching backend. This is automatically enabled and configured as part of HM Platform. See the developer guide for details on how to customise the object cache.

Memcache is also used on WordPress.com VIP, including both VIP Classic and VIP Go.

Fragment Caching Fragment Caching

Fragment caching is a technique which combines page caching and object caching. Rather than caching a full HTML page, fragment caching stores smaller HTML strings (“fragments”) in the object cache. This allows sharing generated HTML between pages and potentially between users.

For example, you might want to fragment-cache a single template part, such as the sidebar or header.

This technique may be useful if the HTML generation itself is expensive, or if caching all the underlying functions used by the template is tricky. It provides an easy-to-implement way to cache, but the downside is that the cached fragment can’t be reused for slightly different output or in other contexts (such as a REST API). Typically, it’s better to use the object cache on the underlying functions instead.

Runtime Caching Runtime Caching

Runtime caching is similar to object caching, but is only stored for a single request. Objects are stored in PHP’s memory until the end of the request and are not persisted across requests. (Runtime caching is sometimes called “local” caching.)

The simplest form of runtime caching in PHP is global or static variables. For example, the $wp_query and $post global variables are runtime caches. Runtime caching is stored entirely in PHP’s memory, so it does not incur the latency overhead of hitting an external object cache server.

In WordPress, a runtime cache is built into the object cache layer, which typically ensures good performance for repeated cache lookups, while still using a persistent object cache across all servers. You can also specifically mark a cache group as non-persistent, which will turn it into a runtime-only cache.

Typically, you don’t need to think about the runtime cache inside the object cache. However, for long-running processes like migrations, data stored in the runtime cache may become stale. Additionally, this runtime cache can use a lot of memory if many items are stored in the runtime cache, and may lead to out-of-memory errors. This may require periodically flushing the runtime cache.

Query Caching Query Caching

Query caching is the lowest level of caching, and takes place entirely within the database. MySQL maintains a cache of SELECT queries with their results, which allows any queries which aren’t cached at a higher level to still be cached.

Query caching is very basic. It caches the result against the exact bytes of the SELECT query, so queries that differ even slightly (such as SELECT vs select) will be stored under separate cache keys. It is unable to cache any queries that contain dynamic functions (such as RAND() or NOW()). Additionally, it is cleared on every update to the table (including INSERT, UPDATE, and DELETE). The query cache cannot be partially invalidated and is controlled at the hosting infrastructure level.

Due to these limitations, the query cache should be regarded as a cache-of-last-resort. It is not a substitute for well-written custom caching code in the object cache, and should not be relied upon. Generally, you should write code as if the query cache does not exist.

For specific queries that should never be cached, you can use the SQL_NO_CACHE option.

The query cache is enabled for sites hosted on HM Cloud and cannot be disabled or configured. The query cache size for HM Cloud databases is 16MB.

Sites hosted on WordPress.com VIP do not have query caching enabled at the database layer. VIP sites have an alternative cache for WP Query calls, which is also called the “query cache”, but is unrelated to the database-layer caching.