- The Real Problem with wp_cache_flush_group()
- Why Is flush_group Dangerous?
- Solution 1: Purge the Exact Key — Simple and Immediately Effective
- Solution 2: Registry Pattern — Automatically Track Keys and Purge Precisely
- Key Tracking Function
- Cache Purge Function
- How to Integrate It into Your Cache Functions
- Trigger Purge When Needed
- Comparing the Two Approaches
- Conclusion
The Real Problem with wp_cache_flush_group()
On the surface, wp_cache_flush_group() looks convenient: a single command wipes an entire cache group. But the problem lies in how it works internally, especially when Redis is used as the object cache backend.
With Redis, wp_cache_flush_group() essentially performs a SCAN operation to find all keys matching that group’s pattern — an O(n) operation across the entire Redis keyspace. If your site has tens of thousands of keys, SCAN can block Redis for several seconds or even cause complete timeouts.
A real-world case: after a user posted a comment, the site called wp_cache_flush_group('init_recent_comments') — Redis crashed, the site went blank, and the request took nearly 6 seconds. After replacing it with a direct wp_cache_delete() on the exact key: instant response, under 1ms.
Why Is flush_group Dangerous?
Three main reasons:
1. Redis SCAN is not atomic. It iterates through the keyspace in batches, meaning it can miss keys added during the scan, or accidentally delete keys from other groups if your naming convention is not strict enough.
2. Not every backend supports it properly. File cache, APCu, and many custom cache backends do not implement flush_group correctly. The result: cache is never actually cleared, and stale data lives forever without you noticing.
3. Over-deletion wastes resources. Flushing an entire group removes cache entries that never needed invalidation in the first place, forcing the system to rebuild everything — causing unnecessary database load.
Solution 1: Purge the Exact Key — Simple and Immediately Effective
If you know exactly which cache key needs to be removed, delete it directly. No group flush, no SCAN, no Redis crashes.
Real example: a recent comments widget with number=10:
add_action( 'wp_insert_comment', function( $comment_id, $comment ) {
if ( ! in_array( $comment->comment_approved, [ '1', 'approve' ], true ) ) {
return;
}
$args = [
'number' => 10,
'status' => 'approve',
'type' => 'comment',
'offset' => 0,
];
$args = apply_filters( 'init_html_recent_comments_query_args', $args );
$key = 'irc_' . md5( maybe_serialize( $args ) );
wp_cache_delete( $key, 'init_recent_comments' );
}, 10, 2 );
This approach is perfect when your cache keys are predictable — for example when they only vary by parameters such as number, paged, or limit.
Solution 2: Registry Pattern — Automatically Track Keys and Purge Precisely
When cache keys are complex and unpredictable — for example keys containing chapter numbers, user IDs, or multiple dynamic parameters — you need a mechanism that automatically tracks every key that has been stored, so you can later purge them accurately.
The idea: every time wp_cache_set() is called, also store that cache key inside a “registry key” within the same group. When it’s time to purge, read the registry, delete each key individually, then delete the registry itself.
Key Tracking Function
/**
* Track a cache key inside the manga registry.
*
* @param int $manga_id
* @param string $key
*/
function init_html_register_chapter_cache_key( int $manga_id, string $key ): void {
$group = "init_html_chapters_{$manga_id}";
$registry_key = "registry_{$manga_id}";
$registry = wp_cache_get( $registry_key, $group ) ?: [];
if ( ! isset( $registry[ $key ] ) ) {
$registry[ $key ] = true;
wp_cache_set( $registry_key, $registry, $group, 0 ); // TTL = 0: never expires
}
}
Cache Purge Function
/**
* Remove all chapter cache for a manga using the registry.
* Does not use wp_cache_flush_group() — safe for every backend.
*
* @param int $manga_id
*/
function init_html_clear_chapter_cache( int $manga_id ): void {
$group = "init_html_chapters_{$manga_id}";
$registry_key = "registry_{$manga_id}";
$registry = wp_cache_get( $registry_key, $group );
if ( ! empty( $registry ) ) {
foreach ( array_keys( $registry ) as $key ) {
wp_cache_delete( $key, $group );
}
wp_cache_delete( $registry_key, $group );
}
}
How to Integrate It into Your Cache Functions
After every wp_cache_set(), add a call to init_html_register_chapter_cache_key():
// Example: get manga chapters
function init_html_get_manga_chapters( int $manga_id, int $limit = 2 ): array {
$group = "init_html_chapters_{$manga_id}";
$key = "manga_chapters_limit_{$limit}";
$cached = wp_cache_get( $key, $group );
if ( false !== $cached ) {
return $cached;
}
// ... query DB ...
wp_cache_set( $key, $data, $group, 10 * MINUTE_IN_SECONDS );
init_html_register_chapter_cache_key( $manga_id, $key ); // track key
return $data;
}
// Example: check for scheduled chapters
function init_html_has_future_schedule( int $manga_id ): bool {
$group = "init_html_chapters_{$manga_id}";
$key = 'has_future_schedule';
// ... query DB ...
wp_cache_set( $key, $has_future, $group, HOUR_IN_SECONDS );
init_html_register_chapter_cache_key( $manga_id, $key ); // track key
return $has_future;
}
// Example: get adjacent chapters (prev/next)
function init_html_get_adjacent_chapters( int $manga_id, float $current_number ): array {
$group = "init_html_chapters_{$manga_id}";
$key = 'adjacent_' . sprintf( '%.2f', round( $current_number, 2 ) );
// ... query DB ...
wp_cache_set( $key, $result, $group, 10 * MINUTE_IN_SECONDS );
init_html_register_chapter_cache_key( $manga_id, $key ); // track key
return $result;
}
Trigger Purge When Needed
// After inserting or deleting a chapter
add_action( 'init_html_after_chapter_inserted', function( int $manga_id ) {
init_html_clear_chapter_cache( $manga_id );
} );
add_action( 'init_html_after_chapter_deleted', function( int $manga_id ) {
init_html_clear_chapter_cache( $manga_id );
} );
Comparing the Two Approaches
wp_cache_flush_group() |
Purge Exact Key | Registry Pattern | |
|---|---|---|---|
| Redis SCAN | ✅ Yes — O(n), dangerous | ❌ No | ❌ No |
| Processing Time | ~6s (can crash) | <1ms | <1ms |
| Supports All Backends | ❌ No | ✅ Yes | ✅ Yes |
| Dynamic/Complex Keys | ✅ Handles them | ❌ Requires hardcoding | ✅ Automatically tracked |
| Accuracy | Over-deletes (entire group) | Perfectly precise | Perfectly precise |
Conclusion
wp_cache_flush_group() is a convenient API on paper, but dangerous in real-world production environments — especially with Redis on high-traffic sites. Instead, follow a simple rule: only delete the exact keys that actually need invalidation — nothing more, nothing less.
If your keys are simple and predictable — purge them directly. If your keys are dynamic and complex — use the registry pattern to track them automatically. Both approaches are safe across all cache backends, avoid Redis overload, and ensure precise cache invalidation without sacrificing performance.
Comments