How WordPress 6.9 fixes query cache bloat

A client called me about a big WooCommerce setup: thousands of products and custom post types for everything under the sun. They kept hitting object cache limits. The admin dashboard was slow, front-end queries would randomly take forever, and their server bills were climbing because the cache kept thrashing. Someone had told them their custom queries were just “too complex” for WordPress caching, but I didn’t buy it. The cause turned out to be inconsistent cache keys, specifically how WordPress handled query groups before 6.9.

For years, WordPress query caches used the ‘last changed’ timestamp as a salt. That sounds fine on paper, but on high-traffic sites that update often, every small post edit changed the timestamp and made previous query caches unreachable. You’d run a query, get a cache miss, regenerate it, save it under a new key, and then the next update would throw that one out too. Worse than inefficient, it was hostile to object caches: they filled up with dead entries that sat there until eviction, which is where the cache bloat came from.

My first instinct, honestly, was to dig into their custom query logic and add some aggressive manual cache invalidation hooks. Hook save_post, clear everything related, done. Seemed logical. But it led to a different kind of hell: race conditions, users seeing stale product data, and sometimes the cache getting hammered even harder because we were clearing it on trivial updates and forcing full re-queries. It was a band-aid on a gushing wound. The fix for consistent cache keys had to go deeper.

WordPress 6.9 finally tackles consistent cache keys

WordPress 6.9 changes this directly. Instead of salting cache keys with the ever-changing ‘last changed’ timestamp, core now stores that timestamp alongside the cached data, so the cache key itself stays consistent. On a query, WordPress reads the existing cache, then checks whether the ‘last changed’ value stored with the entry is still valid. If it’s stale, it regenerates the data, updates the entry, and replaces the old data. Same key, fresh data. The change applies to all the standard query groups: comment-queries, network-queries, post-queries, site-queries, term-queries, and user-queries. You can read more, including the specific changesets, on the WordPress Core development blog.

Core adds new functions: wp_cache_get_salted(), wp_cache_set_salted(), and their multiple counterparts. They’re pluggable, so existing persistent caching drop-ins keep working without changes, though you can optimize if you want to. Here’s how you might wire it into your own code if you work directly with these query caches (and if you’re fighting cache bloat, you probably do):

<?php
/**
 * Custom function to get posts with consistent caching.
 *
 * @param array $args WP_Query arguments.
 * @return array Array of post objects.
 */
function bbioon_get_cached_posts( $args ) {
    $cache_key    = 'bbioon_custom_posts_' . md5( serialize( $args ) );
    $group        = 'post-queries'; // Use the appropriate core query group.
    $last_changed = wp_cache_get_last_changed( $group ); // Get the current last changed value for the group.

    // Check if the new salted functions are available (WP 6.9+).
    if ( function_exists( 'wp_cache_get_salted' ) ) {
        $posts = wp_cache_get_salted( $cache_key, $group, $last_changed );
    } else {
        // Fallback for older WordPress versions.
        // This is where your previous, less efficient caching might have been.
        $posts = wp_cache_get( $cache_key, $group );
        if ( false !== $posts && $posts[0] === $last_changed ) { // Simple manual check if you previously salted
             $posts = $posts[1]; // Get actual data
        } else {
            $posts = false; // Force re-query if stale or no cache
        }
    }

    if ( false === $posts ) {
        $query = new WP_Query( $args );
        $posts = $query->posts;

        // Set the cache using the new salted function or fallback.
        if ( function_exists( 'wp_cache_set_salted' ) ) {
            wp_cache_set_salted( $cache_key, $posts, $group, $last_changed, DAY_IN_SECONDS );
        } else {
            wp_cache_set( $cache_key, array( $last_changed, $posts ), $group, DAY_IN_SECONDS );
        }
    }

    return $posts;
}

// Example usage:
// $cached_posts = bbioon_get_cached_posts( array( 'post_type' => 'product', 'posts_per_page' => 10 ) );
?>

When you upgrade to WordPress 6.9, expect a temporary spike in cache misses. That’s normal, since the keys are changing. It’s worth evicting old, stale cache keys ahead of time for a cleaner transition. For the technical specifics, see the make.wordpress.org dev note on consistent cache keys for query groups in WordPress 6.9.

So what’s the takeaway?

  • WordPress 6.9 changes how query group cache keys work, moving from an ever-changing salt to a consistent key with an in-memory last changed check.
  • That cuts object cache bloat and improves performance on high-traffic sites.
  • If you work directly with core query caches, you’ll need to adapt your code, ideally with the new wp_cache_*_salted functions. Existing drop-ins still work.
  • For most sites it’s a straightforward win for performance and cache stability.

This stuff gets complicated fast. If you’re tired of debugging someone else’s mess and just want your site to work, drop my team a line. We’ve probably seen it before.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.