How to implement object cache with wp_cache in PHP in WordPress

Contents

Introduction — what is WordPress object caching and why wp_cache matters

WordPress provides an object cache layer exposed by the wp_cache family of functions (wp_cache_get, wp_cache_set, wp_cache_add, wp_cache_delete, wp_cache_flush, wp_cache_incr, wp_cache_decr, wp_cache_replace, etc.). Object caching stores PHP values (arrays, objects, scalars) in memory between requests (persistent cache) or for the lifetime of a single request (non-persistent). Properly used, object caching reduces expensive database queries, repeated expensive computations, and remote API calls, improving page generation time and site throughput.

Two modes of object cache

  • Non-persistent (default): A request-scoped cache provided by WP core. It avoids duplicate work in the same request but does not persist between requests.
  • Persistent (drop-in): A custom object-cache.php drop-in placed in wp-content that implements a persistent backend (Redis, Memcached, APCu, file, etc.). WordPress will use that backend across requests.

Core wp_cache functions and parameters explained

Below are the most important functions you will use in themes and plugins. All of them are thin wrappers around the active object cache implementation (the global wp_object_cache instance). When a persistent drop-in is installed, those wrappers call into the drop-in implementation.

  • wp_cache_get(key, group = )

    Fetches a cached value. Returns the value if found returns false when the key is missing. Because false is ambiguous as a stored value, avoid caching boolean false directly or use guard patterns (see examples).

  • wp_cache_set(key, data, group = , expire = 0)

    Stores data under key in cache group group. expire is expiration in seconds (0 means no expiration in many backends, but behavior depends on backend).

  • wp_cache_add(key, data, group = , expire = 0)

    Add only if key does not exist. Returns boolean success.

  • wp_cache_replace(key, data, group = , expire = 0)

    Replaces existing key fails if key does not exist.

  • wp_cache_delete(key, group = )

    Deletes one key.

  • wp_cache_flush()

    Clears entire cache namespace for the backend (use carefully). Persistent backends will remove many keys for shared backends it might only flush keys for your site depending on prefixing.

  • wp_cache_incr(key, offset = 1, group = ) / wp_cache_decr(key, offset = 1, group = )

    Increment/decrement numeric counters stored in cache.

Groups, prefixes and multi-site considerations

Cache groups

Group is a namespace. Use groups to segregate types of cached objects (for example, myplugin-users vs myplugin-settings). Groups keep keys meaningful and make selective invalidation easier.

Prefixes and salts

Persistent backends are typically shared by multiple sites or apps on the same cache server. Use a site-specific prefix/salt (common practice is WP_SITEURL-based salt or a constant) so your keys dont collide with other installations. Many drop-ins provide a configuration constant like WP_CACHE_KEY_SALT or use the blog_id for multisite. If you manage the backend yourself, choose a predictable prefix and set it in the drop-in.

When to use object cache vs transients

  • Object cache (wp_cache): Ideal for high-frequency ephemeral data or database results you want in-memory across requests (if persistent backend installed).
  • Transients (set_transient/get_transient): Stored in options table when no persistent object cache is present. Transients also store expiry metadata and are appropriate when you want DB persistence and expiration on its own.

Cache strategies and common patterns

  • Read-through (lazy caching): On read, if cache miss then compute or query and immediately wp_cache_set. Simple and common.
  • Write-through: Update DB and then update cache immediately (keeps cache fresh).
  • Invalidate on update: Hook into data mutation hooks (save_post, update_option, delete_user, etc.) and delete/update caches. Use group naming to delete all related keys.
  • Key naming: Use prefix ID version: e.g. myplugin:user:123:v2. Incorporate version numbers in keys to perform a logical purge without deleting every key.
  • Dont store huge objects: Very large cache entries cause memory pressure and serialization overhead. Break them into smaller pieces if possible.
  • Avoid caching closures and resource handles: These cannot be serialized and will break between requests.

Using wp_cache in plugin/theme code — practical examples

Below are real-world examples showing safe usage patterns.

Example 1 — Cache a custom database query result (safe boolean handling)

// Language: php
function myplugin_get_popular_products() {
    cache_key = popular_products
    group     = myplugin
    // Try to get value from cache
    cached = wp_cache_get( cache_key, group )
    if ( false !== cached ) {
        // If cached is a value (could be an empty array), return it
        return cached
    }
    // Cache miss: expensive DB query
    global wpdb
    results = wpdb->get_results( SELECT  FROM {wpdb->posts} WHERE post_type = product ORDER BY meta_value_num DESC LIMIT 50, OBJECT )

    // Always store arrays/objects (avoid storing boolean false directly)
    if ( ! results ) {
        results = array() // normalize empty result to array
    }

    // Store in object cache for 10 minutes (600 seconds)
    wp_cache_set( cache_key, results, group, 600 )

    return results
}

Example 2 — Cache with versioned key to invalidate multiple related entries

// Language: php
function myplugin_get_user_dashboard_data( user_id ) {
    version = get_option( myplugin_cache_version, 1 )
    cache_key = dashboard:{user_id}:v{version}
    group = myplugin_dashboard

    data = wp_cache_get( cache_key, group )
    if ( false !== data ) {
        return data
    }

    // Recompute and cache
    data = array(
        stats => myplugin_calculate_user_stats( user_id ),
        recent => myplugin_fetch_recent_items( user_id ),
    )

    wp_cache_set( cache_key, data, group, 300 ) // 5 minutes
    return data
}

// Invalidate all dashboards by bumping version
function myplugin_purge_all_dashboards() {
    version = (int) get_option( myplugin_cache_version, 1 )
    update_option( myplugin_cache_version, version   1 )
}

Example 3 — Hook-based cache invalidation for posts

// Language: php
function myplugin_clean_post_related_cache( post_id, post_after, post_before ) {
    // Delete specific caches for this post
    wp_cache_delete( post:{post_id}, myplugin-posts )

    // Maybe purge lists
    wp_cache_delete( latest_posts, myplugin-posts )
}
add_action( post_updated, myplugin_clean_post_related_cache, 10, 3 )
add_action( deleted_post,  function( post_id ) {
    wp_cache_delete( post:{post_id}, myplugin-posts )
} )

Implementing a persistent object cache — the drop-in object-cache.php

A persistent object cache is enabled by dropping a file named object-cache.php into wp-content. WordPress will include this file early during load and expect it to provide a global wp_object_cache instance implementing the cache methods used by the wp_cache wrappers. Popular backends are Redis, Memcached, and APCu. Many production sites use a battle-tested drop-in (for example, the official Redis or Memcached drop-in plugins). Below are the concepts and a minimal practical example for Memcached to show how a drop-in works.

Important deployment notes for drop-ins

  • Place object-cache.php in wp-content. It overrides WPs internal object cache behavior and is loaded on every request.
  • Make sure PHP extension for your backend is installed (php-redis, php-memcached or php-memcache, APCu, etc.).
  • Choose a key salt/prefix so multiple sites on the same backend dont collide. Common practice: define(WP_CACHE_KEY_SALT, example.com:) in wp-config.php or let the drop-in compute one from site URL.
  • Test on staging first: a buggy drop-in can break admin screens, transient handling, or object serialization.
  • When using object cache with persistent stores, consider serialization options (igbinary serializer for memcached/redis) for speed and memory savings.

Minimal Memcached drop-in example (educational)

This is an educational minimal drop-in showing essential methods. For production use prefer a maintained plugin. Save as wp-content/object-cache.php. The code must be adapted (connection params, error handling, serializer settings) for your environment.

// Language: php
if ( ! class_exists( WP_Object_Cache_Memcached ) ) {

    class WP_Object_Cache_Memcached {
        private memcached
        private global_groups = array()
        private non_persistent_groups = array()
        private cache_key_salt = 

        public function __construct() {
            // Optional salt you can set WP_CACHE_KEY_SALT constant in wp-config.php
            if ( defined( WP_CACHE_KEY_SALT ) ) {
                this->cache_key_salt = WP_CACHE_KEY_SALT
            } else {
                this->cache_key_salt = ( defined( WP_HOME ) ? WP_HOME :  )
            }

            // Connect to Memcached
            this->memcached = new Memcached()
            // Add server(s) - adjust host/port accordingly
            this->memcached->addServer( 127.0.0.1, 11211 )

            // Set reasonable options
            this->memcached->setOption( Memcached::OPT_COMPRESSION, true )
            this->memcached->setOption( Memcached::OPT_TCP_NODELAY, true )
        }

        private function make_key( key, group ) {
            group = group ? group : default
            return this->cache_key_salt . group . : . key
        }

        public function get( key, group =  ) {
            mc_key = this->make_key( key, group )
            value = this->memcached->get( mc_key )
            if ( this->memcached->getResultCode() === Memcached::RES_NOTFOUND ) {
                return false
            }
            return value
        }

        public function set( key, data, group = , expire = 0 ) {
            mc_key = this->make_key( key, group )
            return this->memcached->set( mc_key, data, (int) expire )
        }

        public function add( key, data, group = , expire = 0 ) {
            mc_key = this->make_key( key, group )
            return this->memcached->add( mc_key, data, (int) expire )
        }

        public function replace( key, data, group = , expire = 0 ) {
            mc_key = this->make_key( key, group )
            return this->memcached->replace( mc_key, data, (int) expire )
        }

        public function delete( key, group =  ) {
            mc_key = this->make_key( key, group )
            return this->memcached->delete( mc_key )
        }

        public function flush() {
            return this->memcached->flush()
        }

        public function incr( key, offset = 1, group =  ) {
            mc_key = this->make_key( key, group )
            return this->memcached->increment( mc_key, offset )
        }

        public function decr( key, offset = 1, group =  ) {
            mc_key = this->make_key( key, group )
            return this->memcached->decrement( mc_key, offset )
        }

        // Simple get_multi: takes array of keys, returns associative array
        public function get_multi( keys, group =  ) {
            map = array()
            prefixed = array()
            foreach ( keys as k ) {
                pk = this->make_key( k, group )
                prefixed[] = pk
                map[ pk ] = k
            }
            results = this->memcached->getMulti( prefixed )
            out = array()
            if ( results  is_array( results ) ) {
                foreach ( results as pk => val ) {
                    orig = map[ pk ]
                    out[ orig ] = val
                }
            }
            return out
        }

        // Optionally, expose methods to add global/non-persistent groups
        public function add_global_groups( groups ) {
            this->global_groups = array_merge( this->global_groups, (array) groups )
        }

        public function add_non_persistent_groups( groups ) {
            this->non_persistent_groups = array_merge( this->non_persistent_groups, (array) groups )
        }

    } // end class
}

// Create global instance consumed by WP wrappers
global wp_object_cache
if ( empty( wp_object_cache )  ! wp_object_cache instanceof WP_Object_Cache_Memcached ) {
    wp_object_cache = new WP_Object_Cache_Memcached()
}

Notes about the example:

  • It is intentionally minimal — production drop-ins perform more feature parity with WP expectations (e.g., proper handling of global/non-persistent groups, blog switching for multisite, methods signatures used by newer WP versions, and more lifecycle methods).
  • For persistent caching in production use a tested plugin maintained by the community that implements compatibility across WP versions and offers configuration options (compression, serializers, sentinel support, monitoring, etc.).
  • Pick a serializer (igbinary) where possible to save space and CPU. PHPs default serialize is slower and larger.

Best practices, pitfalls and debugging

  • Do not store closures, DB resources or large DOM-like objects: They wont serialize cleanly and can break between requests.
  • Normalize false/empty results: Avoid storing boolean false as cache value store an empty array or a sentinel object so cache misses vs stored false are distinguishable.
  • Key construction: Use stable, predictable keys. Include IDs and version tokens when appropriate.
  • Expiration: Use TTL for entries that must eventually expire. Understand backend TTL limitations (some backends treat 0 as no expiration, but others may have max TTL values).
  • Evictions: Persistent backends evict keys when memory is full. Monitor eviction counters (Redis INFO, memcached stats) and tune memory or eviction policies.
  • Shared backends: If you share Memcached/Redis across many sites, use salt/prefix to avoid collisions and accidental global flushes.
  • Multisite: For multisite, include blog_id in prefix or use drop-in support for switch_to_blog so keys for different sites don’t collide.
  • Concurrency: Avoid race conditions on heavy cached operations. Consider using wp_cache_add to build a single-writer, or use lock mechanisms implemented on top of your backend (e.g., Redis SETNX).
  • Monitor and test: Use backend statistics and small benchmarking to verify that the persistent cache improves latency. Sometimes misconfiguration makes it slower (bad network latency to cache server, small memory causing thrashing, or heavy serialization CPU).

Testing and validating your object cache

  1. Install your drop-in on staging and enable WP_DEBUG and monitor for warnings/exceptions.
  2. Use backend tools: for Redis use redis-cli INFO, for Memcached use memcached-tool or stats to review hits/misses/evictions.
  3. Benchmark page loads with and without the persistent drop-in enabled. Use ApacheBench, wrk or JMeter to emulate traffic. Keep real user metrics in mind (TTFB, PHP execution time).
  4. Inspect key values to ensure they are what you expect (prefix, serialization, TTL).

Advanced topics

  • Cache warming / priming: For cache-sensitive pages, run a background job to prime caches after deployment or after mass invalidation (creating caches rather than waiting for first user requests).
  • Object cache and full-page caches: Combine object cache for DB/logic results with full-page caches (Varnish, FastCGI caching, CDN) for maximum throughput. Ensure invalidation boundaries are correct.
  • Cache stampedes: When many requests concurrently miss cache, the origin can be overwhelmed. Use locking techniques or a short randomized stagger TTL (jitter) to reduce simultaneous recomputation.
  • Partial caching and fragment caching: Cache only expensive fragments of pages and keep dynamic parts dynamic.
  • Instrumentation: Measure cache hit/miss ratio. Aim for high hit ratio for frequently requested assets and acceptable freshness.

Where to go from here (resources)

  • Official WordPress object cache drop-in plugins (search the plugin repository for Redis / Memcached object cache plugins).
  • Backend documentation:
  • Monitoring and profiling tools for PHP/WordPress (New Relic, Tideways, Xdebug for development).

Summary checklist before enabling persistent object cache in production

  • Choose backend (Redis/Memcached/APCu) and install PHP extension.
  • Install and test a proven drop-in (do not rely purely on example code for production).
  • Set a safe key prefix (WP_CACHE_KEY_SALT) for shared systems.
  • Identify critical cache groups and implement sensible invalidation hooks.
  • Monitor hit/miss ratios and backend health. Tune memory and serialization accordingly.
  • Test on staging and load test to ensure stability.


Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *