Why fetching “10 Latest Comments” can kill MySQL without an Object Cache?

When a site reaches millions of comments, even a simple “10 latest comments” query can scan millions of rows to sort and filter approved ones. The result: high CPU load, slow queries, and terrible TTFB. This article explains why this happens and how to fix it efficiently — by wrapping get_comments() with a persistent Object Cache (Redis/Memcached), normalizing query parameters, and — crucially — purging cache only on delete events to maintain a high cache hit rate at scale.

Why fetching “10 Latest Comments” can kill MySQL without an Object Cache?

Why “10 comments” can still be slow

# Query_time: 8.55s  Rows_sent: 10  Rows_examined: 4,655,080
SELECT wp_comments.comment_ID
FROM wp_comments
JOIN wp_posts ON wp_posts.ID = wp_comments.comment_post_ID
WHERE comment_approved = '1'
  AND comment_type IN ('', 'comment')
  AND wp_posts.post_status IN ('publish')
ORDER BY wp_comments.comment_ID DESC
LIMIT 0,10;

The bottleneck: sorting DESC on comment_ID, joining with wp_posts for post_status filtering, and a massive dataset. You can index aggressively, but the structural fix is reducing query frequency with persistent caching.

Standardized Wrapper: init_html_get_comments_cached()

<?php
/**
 * Retrieve comments with persistent caching.
 * - Relies on Object Cache backend (Redis/Memcached).
 * - Normalizes args to improve cache hit ratio.
 *
 * @param array $args  Same parameters as get_comments().
 * @param int   $ttl   Cache TTL in seconds. Default 1 hour (min 60s).
 * @return array<WP_Comment>|array<int>
 */
function init_html_get_comments_cached( array $args = [], int $ttl = HOUR_IN_SECONDS ) {
    $cache_group = 'init_html_comments';
    $defaults = [
        'number'                     => 10,
        'status'                     => 'approve',
        'type'                       => 'comment',   // exclude pingback/trackback
        'orderby'                    => 'comment_ID',
        'order'                      => 'DESC',
        'update_comment_meta_cache'  => false,
        'update_comment_post_cache'  => false,
    ];
    $args = wp_parse_args( $args, $defaults );

    $raw_key   = 'gc_' . md5( wp_json_encode( $args ) );
    $cache_key = init_html_cache_key_with_version( $raw_key );

    $cached = wp_cache_get( $cache_key, $cache_group );
    if ( false !== $cached ) {
        return $cached;
    }

    $comments = get_comments( $args );
    wp_cache_set( $cache_key, $comments, $cache_group, max( 60, $ttl ) );

    return $comments;
}

Only purge on DELETE: maintain high cache hit ratio

On large-scale sites with millions of comments, purging cache on every event (approve/edit/status change) destroys the hit ratio — making caching almost useless. The correct approach is to purge only on deletion (hard delete) or optionally on trashing if your site hides trashed comments.

<?php
/**
 * Version-key strategy:
 * - If backend supports flush_group → use it.
 * - Otherwise bump version key to invalidate all entries cheaply.
 */
function init_html_flush_comment_group_cache_on_delete() {
    if ( function_exists('wp_cache_supports') && wp_cache_supports('flush_group') ) {
        wp_cache_flush_group('init_html_comments');
        return;
    }
    $v = (int) wp_cache_get('ver', 'init_html_comments');
    wp_cache_set('ver', $v + 1, 'init_html_comments');
}

/** Wrap cache key with version number */
function init_html_cache_key_with_version( string $raw_key ) : string {
    $v = (int) wp_cache_get('ver', 'init_html_comments');
    return $raw_key . ':v' . $v;
}

/**
 * Purge only when deleting:
 * - deleted_comment: permanently removed
 * - trashed_comment: optional if UI hides trashed comments
 */
add_action( 'deleted_comment', 'init_html_flush_comment_group_cache_on_delete', 10 );
add_action( 'trashed_comment', 'init_html_flush_comment_group_cache_on_delete', 10 );

// DO NOT purge on comment_post, edit_comment, or transition_comment_status
// unless you absolutely need real-time updates.

Utility function: “recent approved comments” API

<?php
function init_html_get_recent_approved_comments( int $limit = 10, int $ttl = HOUR_IN_SECONDS ) {
    $args = [
        'number'        => max(1, $limit),
        'status'        => 'approve',
        'type'          => 'comment',
        'orderby'       => 'comment_ID',
        'order'         => 'DESC',
        'post_status'   => 'publish',
        'update_comment_meta_cache' => false,
        'update_comment_post_cache' => false,
    ];
    return init_html_get_comments_cached( $args, $ttl );
}

Note: Consider removing post_status for very large databases

On large-scale WordPress sites with millions of comments, adding a JOIN wp_posts to filter by post_status = 'publish' is often the main reason queries become extremely slow.

In most real-world cases, comments belonging to unpublished posts (drafts, private posts, etc.) are not displayed on the frontend anyway, so filtering by post status at the SQL level is redundant. If performance is critical and you can safely assume only published content appears publicly, remove the post_status parameter from your query.

This allows MySQL to query only the wp_comments table using its primary key index (comment_ID), avoiding the costly join and full-table scan. The result is a dramatic performance improvement — reducing query time from several seconds to a few milliseconds, especially when combined with persistent object caching.

// Instead of:
$comments = get_comments([
    'number'      => 10,
    'status'      => 'approve',
    'post_status' => 'publish', // Heavy JOIN on wp_posts
]);

// Use:
$comments = get_comments([
    'number' => 10,
    'status' => 'approve',
]); // Fast and lightweight

Accepting this small trade-off (skipping the post status filter) has virtually no impact on accuracy in most production environments, but delivers massive gains in query speed and scalability.

Query tuning tips

  • Use fields => 'ids' if you only need comment IDs.
  • Disable update_comment_meta_cache and update_comment_post_cache when not needed.
  • Keep orderby => 'comment_ID' and order => 'DESC' for recent-first lists.

Recommended indexes

  • wp_comments (comment_approved, comment_type, comment_ID) or (comment_approved, comment_type, comment_date_gmt)
  • wp_comments (comment_post_ID, comment_approved) for per-post queries
  • wp_posts (ID, post_status) or (post_status) for join filtering

Integration example

<?php
$comments = init_html_get_recent_approved_comments( 10, 10 * MINUTE_IN_SECONDS );

if ( $comments ) {
    echo '<ul class="recent-comments">';
    foreach ( $comments as $c ) {
        $link    = get_comment_link( $c );
        $author  = get_comment_author( $c );
        $excerpt = wp_html_excerpt( apply_filters('get_comment_text', $c->comment_content, $c), 80, '&hellip;' );
        printf(
            '<li><a href="%s">%s</a>: %s</li>',
            esc_url( $link ),
            esc_html( $author ),
            esc_html( $excerpt )
        );
    }
    echo '</ul>';
}

Practical deployment checklist

  • Enable persistent Object Cache (Redis/Memcached) using a plugin like “Redis Object Cache”.
  • Use init_html_get_comments_cached() everywhere repeated comment queries are made.
  • Purge only on delete (hard delete or optional trash) to maintain cache efficiency.
  • Consider fields => 'ids' + lazy detail loading for lightweight operations.
  • Use a minimum TTL of 60 seconds; increase TTL for less dynamic blocks.

Conclusion

Fetching “10 latest comments” can become a silent performance killer on high-scale WordPress sites. Wrapping queries in a persistent Object Cache and purging only on delete keeps cache hit ratios high, reduces TTFB, and drastically lowers MySQL load — a simple, scalable optimization for any site with heavy user activity.

Comments


  • No comments yet.

Init Toolbox

Press Ctrl + \ on desktop, or swipe left anywhere on mobile.

Login