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.