Blog · Guides

wp_cache_* for Developers: Writing Code That Flies with Redis

WordPress has had an object cache API since nearly the beginning, but a lot of plugin and theme code either ignores it or uses it wrong. That’s a missed opportunity. If you write your data access code against wp_cache_* functions instead of raw queries, your code gets faster the moment it lands on a host with Redis or Memcached configured, with zero changes on your part. This guide walks through the API, the gotchas, and the patterns that actually hold up in production.

Object cache vs. transients vs. raw queries

Three tools solve overlapping problems, and mixing them up leads to bugs:

  • Direct database queries are always correct but always slow relative to memory. Fine for rare admin-only lookups, bad for anything that runs on every front end request.
  • Transients (get_transient / set_transient) are meant for data that should persist across requests with an expiration, and they fall back to the wp_options table when there’s no persistent object cache. That fallback is the trap: on a host without Redis, every transient read and write is a database round trip, sometimes several per page load.
  • The object cache API (wp_cache_get, wp_cache_set, and friends) is the lower level primitive transients are built on. Used directly, it gives you more control over cache groups and lets you make an explicit choice about whether a value needs to survive a server restart or just needs to be fast within a single request.

The practical rule: if the value must survive across page loads even without a persistent cache (like an API token with a real expiration), use a transient. If you’re caching an expensive computation or query result purely for speed, and it’s fine to recompute it if the cache is cold, use the object cache API directly.

The core functions

The API surface is small and easy to hold in your head:

wp_cache_get( $key, $group = '' )
wp_cache_set( $key, $value, $group = '', $expire = 0 )
wp_cache_add( $key, $value, $group = '', $expire = 0 )
wp_cache_delete( $key, $group = '' )
wp_cache_flush_group( $group )

A typical read through pattern looks like this:

function sb_get_expensive_widget_data( $widget_id ) {
    $cache_key = 'widget_data_' . $widget_id;
    $group     = 'sb_widgets';

    $data = wp_cache_get( $cache_key, $group );

    if ( false === $data ) {
        $data = sb_compute_widget_data_from_db( $widget_id );
        wp_cache_set( $cache_key, $data, $group, 5 * MINUTE_IN_SECONDS );
    }

    return $data;
}

A few details matter here. wp_cache_get returns false on a miss, so never cache a value that is legitimately the boolean false without a workaround, or you’ll get cache stampedes where every request thinks it’s a miss. If your data might really be false, wrap it in an array or use the optional $found reference parameter that wp_cache_get accepts as a fourth argument.

Use cache groups deliberately

Groups are not decoration, they’re how you invalidate related data in bulk and how some backends organize storage. Give each plugin or feature its own group name (prefixed to avoid collisions) rather than dumping everything into the default group. This also makes debugging much easier since you can inspect or flush one group without touching everything else.

Why wp_cache_add beats an unconditional set

If a value should only be written once and left alone until it expires or is explicitly invalidated, use wp_cache_add instead of wp_cache_set. wp_cache_add only writes if the key does not already exist, which avoids a subtle race: two concurrent requests both computing the same expensive value and both writing it, wasting work and briefly thrashing the cache backend. It’s a small change with real benefit under load.

Invalidation: the part everyone gets wrong

A cache that never gets invalidated is a bug waiting to surface as stale content. Hook your cache deletes to the actions that change the underlying data:

add_action( 'save_post', function ( $post_id ) {
    wp_cache_delete( 'widget_data_' . $post_id, 'sb_widgets' );
} );

If a single write invalidates many related keys, use wp_cache_flush_group rather than trying to enumerate every possible key. This is one of the strongest arguments for grouping related cache entries together in the first place: invalidation becomes one call instead of a fragile list.

Checking whether a persistent cache is actually there

WordPress ships with a non-persistent in-memory object cache by default. That means your wp_cache_* calls always work, but without a drop-in like Redis or Memcached configured, nothing survives past the current request. Your code should degrade gracefully either way, but if you want to detect the situation (for admin notices, debug logging, or conditional behavior), check:

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

On a host that provides Redis with an object cache drop-in already configured, this returns true out of the box, and every well written wp_cache_* call in your plugin or theme immediately starts benefiting from persistence across requests, without you touching a line of code. That’s the payoff of writing against the API correctly from the start.

Debugging with wp-cli

wp-cli has a small set of cache commands worth knowing:

wp cache get widget_data_42 --network=0
wp cache flush
wp cache type

wp cache type tells you what backend is currently wired up, which is the fastest way to confirm whether you’re looking at Redis, Memcached, or the default non-persistent cache during development or when diagnosing a support ticket.

A note on request scoped caching

Even without any persistent backend, the object cache API still helps within a single page load. If a function gets called multiple times per request (a common pattern in template code, widgets, and REST endpoints), wrapping it in wp_cache_get / wp_cache_set avoids repeat work within that one request even on hosts with no Redis at all. It’s a cheap habit that pays off everywhere.

Takeaway

Writing against wp_cache_* instead of hand rolled static variables or direct queries costs you almost nothing up front and pays off automatically on any host that adds a persistent cache like Redis. Pick expiration times that match how volatile your data actually is, group related keys so invalidation is a single call, use wp_cache_add when you want to avoid stampedes, and check wp_using_ext_object_cache() if your code needs to behave differently depending on what’s available. Do that consistently and your plugin will feel fast everywhere, and blazing fast on infrastructure built for it.