Cache database queries in WordPress with the object cache

WordPress executes dozens of database queries on every page load — fetching options, post data, term relationships, and user meta — and on busy sites these queries can become a significant bottleneck. The WordPress object cache is a built-in in-memory store that allows you to cache the results of expensive operations for the duration of a single page request, eliminating redundant database hits. With a persistent cache backend like Redis or Memcached, the cache survives across requests and can serve cached data to thousands of visitors without a single database query. Even without a persistent backend, the built-in non-persistent cache (which WordPress uses by default) can dramatically reduce the number of queries on a single request by avoiding duplicate lookups. The core API functions are wp_cache_get(), wp_cache_set(), and wp_cache_delete(). For data that should persist across requests but does not require a full object cache server, the Transients API stores values in the database with an expiry time. The object cache and transients complement each other: use the object cache for within-request deduplication and transients for cross-request persistence. Combining both with regular database optimisation and query analysis with EXPLAIN gives you a comprehensive performance strategy. The code below shows practical patterns for caching custom database queries.

Problem: A custom database query runs multiple times per request (or on every page load) and you need to cache its result to reduce database load.

Solution: Add the following code to your functions.php file:

/**
 * Cache a custom DB query for the duration of the request (non-persistent).
 * With a persistent cache backend (Redis/Memcached), this survives across requests.
 */
function helloadmin_get_featured_authors() {
    $cache_key   = 'helloadmin_featured_authors';
    $cache_group = 'helloadmin';

    $authors = wp_cache_get( $cache_key, $cache_group );
    if ( false !== $authors ) {
        return $authors; // cache hit
    }

    global $wpdb;
    $authors = $wpdb->get_results( $wpdb->prepare(
        "SELECT u.ID, u.display_name, COUNT(p.ID) AS post_count
         FROM {$wpdb->users} u
         JOIN {$wpdb->posts} p ON p.post_author = u.ID
         WHERE p.post_status = %s AND p.post_type = %s
         GROUP BY u.ID
         ORDER BY post_count DESC
         LIMIT %d",
        'publish', 'post', 5
    ) );

    wp_cache_set( $cache_key, $authors, $cache_group, HOUR_IN_SECONDS );
    return $authors;
}

/**
 * Invalidate the cache when a post is published or updated.
 */
add_action( 'save_post', 'helloadmin_flush_author_cache' );
function helloadmin_flush_author_cache() {
    wp_cache_delete( 'helloadmin_featured_authors', 'helloadmin' );
}

/**
 * Transient alternative — persists in the DB even without a cache server.
 */
function helloadmin_get_stats() {
    $stats = get_transient( 'helloadmin_site_stats' );
    if ( false !== $stats ) {
        return $stats;
    }
    global $wpdb;
    $stats = [
        'posts' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status='publish' AND post_type='post'" ),
        'users' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->users}" ),
    ];
    set_transient( 'helloadmin_site_stats', $stats, 12 * HOUR_IN_SECONDS );
    return $stats;
}

NOTE: The default WordPress object cache is non-persistent, meaning it is destroyed at the end of each PHP request. To make wp_cache_set() / wp_cache_get() persist across requests, you need to install a persistent cache backend: place a object-cache.php drop-in file in wp-content/ (provided by Redis Object Cache, W3 Total Cache, or similar plugins). Without it, the cache group and expiry arguments to wp_cache_set() are silently ignored.