WordPress has three distinct caching layers that operate at different granularities and serve different performance goals — understanding which layer to use for a given problem determines whether you achieve a 5% improvement or a 10x improvement. The WordPress Object Cache (wp_cache_set(), wp_cache_get()) is an in-memory runtime cache that stores arbitrary PHP values for the duration of a single HTTP request — it prevents duplicate database queries within the same request (e.g., calling get_post(1) 20 times in one request only queries the database once). By default, the object cache is non-persistent (cleared at the end of each request), but a persistent object cache drop-in (Redis or Memcached via a plugin like wp-redis or w3-total-cache) stores the cache across requests, making the same data available to all subsequent requests without hitting the database. The Transients API (set_transient(), get_transient()) is a higher-level persistent cache that stores values in the WordPress options table (or the persistent object cache if one is available) with a time-based expiration — appropriate for caching the result of a slow external API call, a complex database query whose result changes infrequently, or any computed value that takes more than 100ms to regenerate. Full-page caching (Nginx fastcgi_cache, WP Super Cache, WP Rocket) caches the entire HTML output of a WordPress page — serving repeat requests from a cached HTML file without executing any PHP, which reduces response time from 200–800ms to 5–20ms. Full-page caching is the highest-impact optimization but requires careful cache invalidation strategy for pages that change frequently (WooCommerce product pages, pages with user-specific content). The hierarchy: full-page cache prevents PHP execution entirely; persistent object cache reduces database queries within PHP execution; transients cache slow computations within PHP; the non-persistent object cache deduplicates queries within a single request. The Transients API post showed transient usage patterns; this post shows where each cache layer fits in the overall performance stack and how they interact.
Problem: A WordPress site takes 800ms to generate the homepage — profiling shows 60% of the time is spent in a WP_Query that joins 4 tables and runs a full-text search across 50,000 posts, called on every request including when the result has not changed in hours. A Redis cache plugin is installed but the homepage is not cached because it contains a personalized “Recently viewed” widget.
Solution: Cache the expensive WP_Query result with a transient, separate the personalized “Recently viewed” section into a client-side fetch so the static homepage content can be full-page cached, and use the object cache for deduplication of repeated queries within the request.
// ── Layer 1: Transient cache for the slow expensive query ─────────────────
function myplugin_get_featured_posts(): array {
$cache_key = 'myplugin_featured_posts_v1';
// Check transient first (stored in Redis via persistent object cache)
$posts = get_transient( $cache_key );
if ( false !== $posts ) {
return $posts;
}
// Cache miss: run the expensive query
$query = new WP_Query( [
'post_type' => 'post',
'posts_per_page' => 12,
'post_status' => 'publish',
'meta_key' => '_featured_score',
'orderby' => 'meta_value_num',
'order' => 'DESC',
'ignore_sticky_posts' => true,
'no_found_rows' => true, // skip COUNT(*) — improves speed when pagination not needed
'update_post_meta_cache' => true,
'update_post_term_cache' => true,
] );
$posts = $query->posts;
// Store in transient for 1 hour (or until a post is published/updated)
set_transient( $cache_key, $posts, HOUR_IN_SECONDS );
return $posts;
}
// Invalidate transient when any post is published or updated
add_action( 'save_post', function( int $post_id ): void {
if ( get_post_status( $post_id ) === 'publish' ) {
delete_transient( 'myplugin_featured_posts_v1' );
}
} );
// ── Layer 2: Non-persistent object cache for per-request deduplication ────
function myplugin_get_post_view_count( int $post_id ): int {
$cache_key = "post_views_{$post_id}";
// Object cache prevents repeated queries within a single request
$cached = wp_cache_get( $cache_key, 'myplugin' );
if ( false !== $cached ) {
return (int) $cached;
}
global $wpdb;
$count = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT view_count FROM {$wpdb->prefix}post_views WHERE post_id = %d",
$post_id
) );
wp_cache_set( $cache_key, $count, 'myplugin', 300 ); // 5 min TTL
return $count;
}
// ── Layer 3: Render recently-viewed as async JS (enables full-page caching) ─
// Output a placeholder div; JavaScript fills it from a separate non-cached endpoint
function myplugin_render_recently_viewed_placeholder(): void {
echo '
';
}
// REST endpoint for recently viewed (excluded from full-page cache by URL pattern)
add_action( 'rest_api_init', function(): void {
register_rest_route( 'myplugin/v1', '/recently-viewed', [
'methods' => 'GET',
'callback' => 'myplugin_get_recently_viewed',
'permission_callback' => '__return_true',
] );
} );
function myplugin_get_recently_viewed( WP_REST_Request $request ): WP_REST_Response {
$ids = array_map( 'absint', explode( ',', $request->get_param( 'ids' ) ?? '' ) );
$ids = array_filter( $ids );
if ( ! $ids ) return new WP_REST_Response( [] );
$posts = get_posts( [ 'post__in' => $ids, 'posts_per_page' => 5, 'orderby' => 'post__in' ] );
return new WP_REST_Response( array_map( function( $p ) {
return [ 'id' => $p->ID, 'title' => $p->post_title, 'link' => get_permalink( $p->ID ) ];
}, $posts ) );
}
NOTE: When a persistent object cache (Redis/Memcached) is installed, WordPress automatically uses it as the backing store for both wp_cache_* calls AND the Transients API — set_transient() stores the value in Redis with the TTL, not in the options table. This means get_transient() is essentially a wrapper around wp_cache_get() when Redis is active, and the two APIs are not duplicating storage. The key difference is that transients are always persistent (surviving server restarts if Redis uses persistence), while wp_cache_* without a persistent drop-in is request-scoped only. Always check for a persistent cache plugin when diagnosing why transients are slow — a site without Redis stores every transient as a database row in wp_options, and 1000+ transient rows without autoload disabled can degrade the options table query significantly.