Object caching and persistent caches

Cache expensive results with the wp_cache functions, add a persistent backend, and invalidate cached data reliably.

  • Intermediate
  • 9 min read
  • Updated 17 Oct 2026

What you will learn

  • Cache data with wp_cache functions
  • Add a persistent object cache
  • Organise keys with cache groups
  • Invalidate cached data when the source changes

Before you start

The object cache stores the results of work WordPress has already done, such as a loaded post, an option or the result of a custom query, so the same request does not repeat it. Used well, it removes database queries from every page. Used carelessly, it serves stale data. This lesson covers both sides.

General caching theory (cache-aside, TTLs, stampedes) has its own course. Here we focus on how WordPress implements it.

What the object cache is

WordPress always has an object cache. Core uses it constantly: when you call get_post(), get_option() or get_post_meta(), the result is stored in the cache, and the second call in the same request is answered from memory.

By default the cache is non-persistent. It is a PHP array inside the WP_Object_Cache class, and it disappears when the request ends. Every new page load starts empty.

A persistent object cache keeps those values between requests in an in-memory store such as Redis or Memcached. WordPress supports this through a drop-in file, wp-content/object-cache.php. When that file exists, WordPress loads it instead of the default cache class, and every wp_cache_* call goes to the external store.

Default cachePersistent cache
StoragePHP memoryRedis, Memcached or similar
LifetimeOne requestUntil expiry or eviction
SetupNothingServer software plus object-cache.php drop-in
HelpsRepeated lookups within a pageRepeated lookups across all visitors

You can check which one is active in code:

PHP
if ( wp_using_ext_object_cache() ) {
    // A persistent object cache drop-in is loaded.
}

From the terminal, wp cache type reports the cache type WP-CLI detects.

The core functions

The API is small. These are the functions you will use most:

FunctionWhat it does
wp_cache_get( $key, $group, $force, &$found )Read a value. Returns false on a miss.
wp_cache_set( $key, $data, $group, $expire )Store a value, overwriting any existing one.
wp_cache_add( $key, $data, $group, $expire )Store only if the key does not exist yet.
wp_cache_replace( $key, $data, $group, $expire )Store only if the key already exists.
wp_cache_delete( $key, $group )Remove a value.
wp_cache_incr( $key, $offset, $group )Increase a numeric value.
wp_cache_get_multiple( $keys, $group )Read several keys in one call.

$expire is in seconds. 0 means "no expiry", which for a persistent cache means the value stays until it is deleted or evicted for memory.

A complete caching example

The usual pattern is "check the cache, otherwise compute and store". Here a plugin counts published events per venue, which needs a slow meta query.

PHP
<?php
/**
 * Plugin Name: My Plugin Venue Stats
 */

defined( 'ABSPATH' ) || exit;

function myplugin_get_venue_event_count( int $venue_id ): int {
    $found = false;
    $count = wp_cache_get( 'venue_count_' . $venue_id, 'myplugin_venues', false, $found );

    if ( $found ) {
        return (int) $count;
    }

    $query = new WP_Query(
        array(
            'post_type'      => 'myplugin_event',
            'post_status'    => 'publish',
            'posts_per_page' => 1,
            'fields'         => 'ids',
            'meta_key'       => 'myplugin_venue_id',
            'meta_value'     => $venue_id,
        )
    );

    $count = (int) $query->found_posts;

    wp_cache_set( 'venue_count_' . $venue_id, $count, 'myplugin_venues', HOUR_IN_SECONDS );

    return $count;
}

Two details matter:

  • The $found argument. wp_cache_get() returns false on a miss, but false (or 0) can also be a real cached value. Passing $found by reference tells you which it was. Without it, a venue with zero events would never be cached.
  • The group. myplugin_venues keeps your keys apart from core's and lets a backend treat them together.

Cache the smallest useful result: a count, an array of IDs, a rendered fragment. Caching whole WP_Query objects stores far more than you need and can include data you did not mean to keep.

Cache groups

Every key lives in a group. Core uses groups such as posts, post_meta, options, terms and users. Prefix your own groups like everything else in a plugin.

Two functions change how groups behave with a persistent cache:

PHP
add_action( 'init', 'myplugin_register_cache_groups' );

function myplugin_register_cache_groups(): void {
    // Never send this group to the persistent store. It lives for one request only.
    wp_cache_add_non_persistent_groups( array( 'myplugin_request' ) );

    // On Multisite, share this group across every site in the network.
    wp_cache_add_global_groups( array( 'myplugin_network' ) );
}

Use non-persistent groups for data that is cheap to build but used many times in one request, or that must never be shared, such as values tied to the current user's session.

Invalidation: the hard part

Cached data is a copy. When the original changes, the copy must go. There are three common approaches.

Delete on change

Remove the key when the source data changes. For the venue count, an event being saved, trashed or deleted changes the number.

PHP
add_action( 'save_post_myplugin_event', 'myplugin_flush_venue_count', 10, 1 );
add_action( 'before_delete_post', 'myplugin_flush_venue_count', 10, 1 );

function myplugin_flush_venue_count( int $post_id ): void {
    $venue_id = (int) get_post_meta( $post_id, 'myplugin_venue_id', true );

    if ( $venue_id > 0 ) {
        wp_cache_delete( 'venue_count_' . $venue_id, 'myplugin_venues' );
    }
}

This is precise but easy to get wrong: if the venue meta changes from 4 to 7, you need to clear both counts. Map every path that changes the data before you trust it.

Versioned keys with last_changed

Core uses a cleverer pattern for queries. It stores a last_changed value per group and includes it in cache keys. When any post changes, core updates last_changed for the posts group, so every old key is simply never read again.

PHP
function myplugin_get_featured_ids(): array {
    $last_changed = wp_cache_get_last_changed( 'posts' );
    $key          = 'featured_ids:' . $last_changed;

    $ids = wp_cache_get( $key, 'myplugin_queries' );

    if ( false === $ids ) {
        $ids = get_posts(
            array(
                'post_type'      => 'post',
                'posts_per_page' => 10,
                'fields'         => 'ids',
                'meta_key'       => 'myplugin_featured',
                'meta_value'     => '1',
                'no_found_rows'  => true,
            )
        );
        wp_cache_set( $key, $ids, 'myplugin_queries', DAY_IN_SECONDS );
    }

    return $ids;
}

Old entries are left to expire. Always set an expiry with this pattern so the store does not fill with dead keys.

Short expiry

For data you cannot track, such as a remote API, accept slight staleness and set a short $expire. This is the honest option when you cannot know when the source changes.

Flushing

wp_cache_flush() empties the whole cache. On a persistent backend shared by several sites, that can mean all of them. Avoid calling it from plugin code.

To clear one group, current versions of WordPress provide wp_cache_flush_group(), but not every backend implements it. Check first:

PHP
if ( wp_cache_supports( 'flush_group' ) ) {
    wp_cache_flush_group( 'myplugin_venues' );
}

From WP-CLI:

Shell
wp cache flush
wp cache get venue_count_12 myplugin_venues
wp cache delete venue_count_12 myplugin_venues

WP-CLI runs in its own PHP process. Without a persistent cache, wp cache get only sees that process's empty cache, so it tells you nothing about the web requests. The commands are useful only when a persistent backend is installed.

Adding a persistent backend

The setup has three parts:

  1. Run the store. Install Redis or Memcached on the server, or use a managed service. Keep it on a private network, never exposed to the internet.
  2. Install the PHP extension the drop-in needs, for example phpredis or memcached.
  3. Install the object-cache.php drop-in. Free, well maintained plugins exist for common backends; they place the drop-in and add a status screen. Their configuration usually lives in wp-config.php constants, documented by each plugin.

If several WordPress sites share one Redis instance, give each a unique key prefix or database number so their keys never collide. The drop-in's documentation names the constant to set.

Transients and the object cache

Transients from the previous lesson change behaviour once a persistent cache is active. set_transient() stores the value in the object cache instead of the wp_options table. That makes them faster, but it also means a transient can disappear at any time when the store evicts it under memory pressure.

The rule has always been the same: treat a transient or cache entry as something that might be missing, and always be able to rebuild it.

What not to cache

  • Per-user data in shared groups. Include the user ID in the key, or use a non-persistent group.
  • Data that must not be lost. The cache is not storage. Orders, form entries and settings belong in the database.
  • Very large values. Backends have item size limits and big values slow down every read.
  • Things that are already cheap. Core already caches posts, meta and options. Wrapping get_option() in your own cache adds nothing.

Summary

  • WordPress always has an object cache; by default it lasts one request.
  • A persistent cache such as Redis or Memcached is added with the object-cache.php drop-in, and then every wp_cache_* call persists between requests.
  • Use wp_cache_get() with the $found argument, then wp_cache_set() with a prefixed group and a sensible expiry.
  • Invalidate by deleting keys on change, by versioning keys with wp_cache_get_last_changed(), or with short expiries.
  • Avoid wp_cache_flush() in plugin code; prefer wp_cache_flush_group() where wp_cache_supports( 'flush_group' ) is true.
  • Cached values and transients can vanish at any time, so code must always be able to rebuild them.

Further reading