WordPress Object Cache Groups and Cache Invalidation Strategies

WordPress’s object cache API supports cache groups — named namespaces that let you flush a logical set of entries atomically without clearing the entire cache. Combined with a persistent backend (Redis or Memcached), groups are the right tool for caching expensive queries while ensuring stale data is never served after content changes.

Problem: WordPress object cache groups allow logical partitioning, but without a clear invalidation strategy, stale data is served after post updates, term changes, or plugin settings saves — and a cache flush nukes everything rather than just the affected data.

Solution: Group related cache entries with a cache key prefix that includes a version number stored in a separate cache key — increment the version to invalidate the group without flushing the entire cache. Hook into save_post, edited_term, and update_option to invalidate only the affected groups. Use wp_cache_delete_group() when available.


The code below shows how to cache a custom query in a dedicated group, how to use a generation key pattern for instant group-level invalidation without iterating over keys, and how to register non-persistent groups for request-scoped data.


<?php
// ── 1. Store and retrieve in a named group ────────────────────────────────
function get_popular_posts( int $limit = 5 ): array {
    $group = 'popular_posts';
    $key   = "popular_{$limit}";

    $cached = wp_cache_get( $key, $group );
    if ( false !== $cached ) {
        return $cached;
    }

    $posts = get_posts( [
        'numberposts'    => $limit,
        'meta_key'       => 'post_views',
        'orderby'        => 'meta_value_num',
        'order'          => 'DESC',
        'no_found_rows'  => true,
    ] );

    wp_cache_set( $key, $posts, $group, HOUR_IN_SECONDS );
    return $posts;
}

// ── 2. Generation-key pattern: invalidate the whole group in O(1) ─────────
function get_popular_posts_v2( int $limit = 5 ): array {
    // The generation key increments on any invalidation — cached items
    // under the old generation are automatically stale and never served.
    $gen   = (int) wp_cache_get( 'popular_posts_gen', 'cache_generations' );
    $group = "popular_posts_{$gen}";
    $key   = "popular_{$limit}";

    $cached = wp_cache_get( $key, $group );
    if ( false !== $cached ) {
        return $cached;
    }

    $posts = get_posts( [
        'numberposts'   => $limit,
        'meta_key'      => 'post_views',
        'orderby'       => 'meta_value_num',
        'order'         => 'DESC',
        'no_found_rows' => true,
    ] );

    wp_cache_set( $key, $posts, $group, DAY_IN_SECONDS );
    return $posts;
}

// Invalidate all popular-post cache entries by bumping the generation key
function invalidate_popular_posts_cache(): void {
    $gen = (int) wp_cache_get( 'popular_posts_gen', 'cache_generations' );
    wp_cache_set( 'popular_posts_gen', $gen + 1, 'cache_generations', 0 );
}
add_action( 'save_post', 'invalidate_popular_posts_cache' );

// ── 3. Non-persistent group: lives only for the current HTTP request ───────
wp_cache_add_non_persistent_groups( [ 'my_request_scope' ] );

function get_current_user_permissions(): array {
    $key    = 'perms_' . get_current_user_id();
    $cached = wp_cache_get( $key, 'my_request_scope' );
    if ( false !== $cached ) {
        return $cached;
    }
    $perms = compute_expensive_permissions();   // custom function
    wp_cache_set( $key, $perms, 'my_request_scope' );
    return $perms;
}


NOTE: The generation-key pattern works even with Redis cluster mode, where you cannot use key-pattern scans or FLUSHDB — incrementing a single small key is an O(1) operation that effectively voids the old generation without touching any other cache entries.