Queries Caching: WP_Query (WP 6.1 ) WP_Term_Query (WP 6.4)
Since WordPress 6.1, caching of certain database queries, specifically WP_Query, has been added to the WordPress core. It is enabled by default.
From WordPress 6.4 onwards, similar caching was added for WP_Term_Query queries, also enabled by default.
This feature helps improve performance. However, when using persistent object caching, the cache size increases significantly, which can become a bigger issue than the additional database load.
If you have a large site and you're using an object caching plugin (e.g., Redis Cache), this default caching is likely to cause more harm than good.
How to disable caching?
Database query caching is enabled by default in WP_Query and WP_Term_Query.
Completely disabling caching
If you need to globally disable caching for all queries, use the filters parse_query and pre_get_terms:
/// Disable WP_Query caching: by default WP caches WP_Query results since WP 6.1
add_action( 'pre_get_posts', 'disable_get_posts_caching' );
function disable_get_posts_caching( $wp_query ) {
$wp_query->query_vars['cache_results'] = false;
}
/// Disable WP_Term_Query caching.
add_action( 'pre_get_terms', 'disable_get_terms_caching' );
function disable_get_terms_caching( $wp_term_query ) {
$wp_term_query->query_vars['cache_results'] = false;
}
Disabling term caching this way is not recommended, as it may reduce performance. Instead, consider setting a limited cache lifetime (see below).
Caching for terms has always existed. Since WP 6.2, it was moved to a separate cache group term-queries, and in 6.4, the cache_results parameter was introduced, allowing caching to be disabled.
Disabling caching for specific queries
Specify the parameter cache_results=false:
$query = new WP_Query( [ 'posts_per_page' => 50, 'cache_results' => false, ] ); $terms = get_terms( [ 'taxonomy' => 'post_tag', 'hide_empty' => false, 'cache_results' => false, ] );
Setting a shorter Time-To-Live (TTL)
By default, cache entries have no expiry and may grow indefinitely.
Instead of completely disabling caching, you can reduce cache size by setting a maximum lifetime (TTL) for such cache entries.
All persistent caching plugins should have an appropriate hook.
Let's consider the redis-cache plugin as an example. It has a hook for our needs called redis_cache_expiration:
/** * Filters the cache expiration time * * @since 1.4.2 * @param int $expiration The time in seconds the entry expires. 0 for no expiry. * @param string $key The cache key. * @param string $group The cache group. * @param mixed $orig_exp The original expiration value before validation. */ $expiration = apply_filters( 'redis_cache_expiration', $expiration, $key, $group, $orig_exp );
Cache groups we need: term-queries and post-queries:
/// Shorter cache_expiration time (TTL) for some groups of cache.
add_filter( 'redis_cache_expiration', 'set_post_term_queries_cache_ttl', 10, 3 );
function set_post_term_queries_cache_ttl( $ttl, $key, $group ) {
static $short_ttl_groups = [
'term-queries',
'post-queries',
'site-queries',
'comment-queries',
];
if( in_array( $group, $short_ttl_groups, true ) ){
return HOUR_IN_SECONDS * 6;
}
return $ttl;
}
Exclusion by key:
/// Shorter cache_expiration time (TTL) for some groups of cache.
add_filter( 'redis_cache_expiration', 'set_post_term_queries_cache_ttl', 10, 3 );
function set_post_term_queries_cache_ttl( $ttl, $key, $group ) {
if( preg_match( '/get_terms:|wp_query:|get_users:/', $key ) ){
return HOUR_IN_SECONDS * 6;
}
return $ttl;
}
You can see which keys are used in the code:
To verify everything works correctly: clear the cache, connect to Redis, and check the TTL of a single entry.
Example:
/data # redis-cli 127.0.0.1:6379> 127.0.0.1:6379> 127.0.0.1:6379> KEYS '*get_terms*' 1) "hl_1:term-queries:get_terms-bfa6333d60e0407f94a7fb93d5c9af0a-0.69511300 1740324474" 127.0.0.1:6379> 127.0.0.1:6379> 127.0.0.1:6379> TTL 'hl_1:term-queries:get_terms-bfa6333d60e0407f94a7fb93d5c9af0a-0.69511300 1740324474' (integer) 21573 127.0.0.1:6379>
Here we can see a TTL of 21573 (seconds) — that's 21573 ÷ 3600 = 5.99 hours.
Redis tracks the Time-To-Live (TTL) of each key separately.
When a key is stored with TTL, Redis:
- Periodically checks keys and automatically deletes expired ones.
If a key hasn't expired, it remains in memory and accessible. - Returns nothing when attempting to retrieve a key with expired TTL.
Redis doesn't remove keys exactly at the expiration moment; it gradually deletes them in the background to minimize load.
Caching Queries in WP_Query
With WordPress 6.1, database queries implemented in the WP_Query class are cached. Therefore, if the same database query is executed more than once, subsequent results will be loaded from the cache.
If your site uses persistent object caching, database queries will be cached in the object cache and will not be executed until the cache is cleared. All of this reduces the number of database queries on the site as a whole.
If persistent caching is not set up (memory caching is in use), the site will also benefit because repeated queries on the page will not be executed but will be taken from the cache. However, performance improvement will not be as significant in this case.
For Developers
Developers need to ensure that built-in WP functions, such as wp_insert_post(), are used for adding or updating posts. These functions will ensure proper cache invalidation.
If you are updating the database directly, after updating a row in the database, make sure to clear the post cache using the clean_post_cache() function.
Cache Key (How It Is Determined Whether Cache Exists)
Whether a cache exists or not is determined by comparing the cache key.
The cache key is created from the parameters of the query passed to WP_Query{}. However, the following parameters are ignored:
suppress_filters cache_results fields update_post_meta_cache update_post_term_cache update_menu_item_cache lazy_load_term_meta
These parameters do not affect the database query. The most important parameter to note is fields. This means that if you execute the following:
$query1 = new WP_Query( [ 'posts_per_page' => 50, 'fields' => 'ids' ] ); $query2 = new WP_Query( [ 'posts_per_page' => 50, 'fields' => 'all' ] );
In both cases, the query will now request all fields so that the result can be cached and then used regardless of the fields parameter.
Prior to this change, the database query for these two cases was different, but maintaining this would lead to multiple caches for actually subsets of the same data. This means that now the performance improvement when limiting fields to ids will be less than in the previous version of WordPress.
This change also means that the parameters (caches) update_post_meta_cache and update_post_term_cache are always considered.
Now Unnecessary Plugins
It is recommended to disable and remove plugins that add caching functionality to WP_Query. For example:
- advanced-post-cache
- Cache WP_Query
- Enhanced Post Cache
For more information, see the Trac ticket #22176.
Caching Users in WP_Query
In WordPress 6.1, a new function, update_post_author_caches(), was introduced.
Before version 6.1, multi-author sites required several separate database queries to retrieve author information. This happened because user data was loaded during the loop.
Now, instead of loading each user separately, data for all users present in the loop is loaded in one query at the beginning of the loop - see update_post_author_caches(). This results in significantly fewer database queries.
Calls to update_post_author_caches() have also been added in other key places in the core code. This has increased performance.
For more information, see the Trac ticket #55716
Caching Related Objects for Menu Items
A new function update_menu_item_cache() has been added to the core. It takes an array of post objects and caches the post or term objects that menu items refer to.
A new parameter update_menu_item_cache has been added for WP_Query. If set to true, it will call update_menu_item_cache(), allowing you to cache menu items in two database queries (one for posts and one for terms).
For more information, see the Trac ticket #55620
--