Caching & the Object Cache

How wp_cache_get and wp_cache_set work, why they are per-request until you add a drop-in, and how page caching differs.

Syntaxwp_cache_get( int|string $key, string $group = '', bool $force = false, bool &$found = null ): mixed|false

The object cache is WordPress's key/value store for anything expensive to fetch or compute, and most of core sits on top of it. Out of the box it is not persistent: WP_Object_Cache is a PHP array that exists for the length of one request and is discarded when the response is sent. Its value in that default state is deduplication — the second get_option() or get_post() for the same key costs nothing.

Making it persistent

Placing an object-cache.php file in wp-content/ replaces that class wholesale with a Redis, Memcached or APCu backend. It is a drop-in, not a plugin: wp-settings.php loads it before plugins, and it cannot be activated or deactivated from the admin — it is only listed, read-only, under the Plugins screen's Drop-ins tab. Your wp_cache_* calls do not change; they simply start surviving between requests, and transients move out of wp_options into the same backend.

What core already caches

Every autoloaded option arrives as one alloptions entry in the options group, fetched by wp_load_alloptions(). Posts, terms, meta and users are primed in bulk by WP_Query so the loop does not query per post. Groups namespace keys, so the same key in two groups is two separate entries.

Page caching is a different layer

Defining WP_CACHE as true makes wp-settings.php include wp-content/advanced-cache.php before most of WordPress loads, letting a full-page cache return stored HTML without ever running the query. Object caching makes a request cheaper; page caching avoids the request entirely.

Example

Example · php
<?php
/**
 * An expensive per-user report, cached in the object cache.
 * The 'acme' group namespaces the key so it cannot collide with core's.
 */
function acme_get_report( int $user_id ): array {
    $key   = "report_{$user_id}";
    $found = false;

    // $found is passed by reference (4th parameter). It is the only way to tell
    // "we cached the value false" apart from "nothing was cached", because both
    // make wp_cache_get() return false.
    $report = wp_cache_get( $key, 'acme', false, $found );
    if ( $found ) {
        return $report;
    }

    $report = acme_build_report( $user_id ); // several seconds of SQL

    // wp_cache_set( $key, $data, $group = '', $expire = 0 ) — 0 means no expiry.
    // Always overwrites; use wp_cache_add() if you only want to set on a miss.
    wp_cache_set( $key, $report, 'acme', 15 * MINUTE_IN_SECONDS );

    return $report;
}

// Invalidate on write rather than relying on a short expiry.
add_action( 'profile_update', function ( int $user_id ): void {
    wp_cache_delete( "report_{$user_id}", 'acme' );
} );

When to use it

  • A dashboard widget runs a five-second aggregate query on every admin page load.
  • get_option() is called for the same key in six plugins and you want to confirm only one query runs.
  • You are installing Redis on a site and need to know what changes and what does not.
  • A settings save must invalidate every cached report without flushing core's post caches.

More examples

One call instead of N: wp_cache_get_multiple

The shorthand and longhand produce identical arrays; the difference is network round trips, which is invisible on the default in-memory cache and very visible once a Redis drop-in is installed. Reach for the multiple form any time you are looping over a known set of IDs.

Example · php
<?php
$ids  = array( 12, 34, 56 );
$keys = array_map( fn( int $id ): string => "widget_{$id}", $ids );

// Longhand: one lookup per key. Against Redis or Memcached that is N round trips.
$objects = array();
foreach ( $keys as $key ) {
    $objects[ $key ] = wp_cache_get( $key, 'acme' );
}

// Shorthand for exactly the same result, in a single batched request.
// wp_cache_get_multiple( array $keys, string $group = '', bool $force = false ): array
// Added in WordPress 5.5. Misses come back as false under their key — the key is
// always present in the returned array, so use array_keys(), not isset().
$objects = wp_cache_get_multiple( $keys, 'acme' );

foreach ( $objects as $key => $value ) {
    if ( false === $value ) {
        // Rebuild this one and wp_cache_set() it.
    }
}

Flushing a group safely

Use this when a bulk import or a settings change invalidates a whole family of cached values at once. The versioned-prefix fallback is worth knowing regardless: it is O(1), it needs no backend support, and it never risks flushing core's caches the way wp_cache_flush() does.

Example · php
<?php
function acme_flush_reports(): void {
    // wp_cache_flush_group() was added in WordPress 6.1, but not every backend
    // implements it — the docs tell you to check support first, otherwise the
    // call silently does nothing and you ship a stale-cache bug.
    if ( wp_cache_supports( 'flush_group' ) ) {
        wp_cache_flush_group( 'acme_reports' );
        return;
    }

    // Fallback that works everywhere: version the key prefix. Bumping the number
    // orphans every old key at once and lets them expire on their own.
    $version = (int) get_option( 'acme_reports_cache_version', 1 );
    update_option( 'acme_reports_cache_version', $version + 1, false ); // false = do not autoload
}

Keeping scratch data out of the persistent backend

Declaring a group non-persistent is how you get deduplication within a request without leaking request-specific state across requests or across users. Register it on init, before anything writes to the group — a drop-in only checks the list at write time.

Example · php
<?php
add_action( 'init', function (): void {
    // Per-request working data — half-built objects, request-scoped lookups —
    // must never be written to Redis, where another request could read it.
    // wp_cache_add_non_persistent_groups( string|string[] $groups ): void
    // A no-op against the default in-memory cache; honoured by real drop-ins.
    wp_cache_add_non_persistent_groups( array( 'acme_request' ) );
} );

function acme_current_context(): array {
    $context = wp_cache_get( 'context', 'acme_request' );
    if ( false === $context ) {
        $context = acme_resolve_context(); // depends on the current request
        wp_cache_set( 'context', $context, 'acme_request' );
    }
    return $context;
}

Discussion

  • Be the first to comment on this lesson.