Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 039 / 100

Object caching with Redis: practical tips and trade-offs

Faster dynamic pages.

Expona Admin7 min read

Object caching with Redis: practical tips and trade-offs

Most WordPress sites serve dynamic content—user meta, post queries, WooCommerce carts, plugin settings—that gets regenerated on every request. When you have thousands of logged-in users or complex post loops, that repetitive database work becomes a bottleneck. Object caching with Redis is one of the highest-ROI optimizations we've deployed in production, and in this article we'll walk through installation, real-world trade-offs, and the monitoring discipline that keeps it from becoming a liability.

Install a Redis object cache drop-in

WordPress's object cache API is designed to be pluggable. By default, objects are cached in memory within a single request—they disappear after PHP exits. A persistent object cache backend like Redis keeps those objects across requests, sessions, and users.

We typically use the Redis Object Cache Pro or the free WordPress Object Cache plugin by Till Krüss. The setup is straightforward:

  1. Ensure Redis is running on your server (port 6379 by default, or a socket).
  2. Install the drop-in plugin or upload the object cache file directly to wp-content/.
  3. Test the connection via the WordPress admin dashboard or CLI.

Here's what a basic Redis connection looks like in code:

php
// wp-config.php or drop-in configuration
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_PASSWORD', '' );
define( 'WP_REDIS_DB', 0 );
define( 'WP_CACHE_KEY_SALT', 'my-site-' );

The salt prevents key collisions if you run multiple WordPress installs against the same Redis instance. Once connected, every wp_cache_set() and wp_cache_get() call will hit Redis instead of PHP memory.

Verify the connection in your theme or plugin:

php
// Quick test
wp_cache_set( 'test_key', 'test_value', 'test_group', 300 );
$value = wp_cache_get( 'test_key', 'test_group' );
echo $value; // Should output: test_value

If this works, Redis is live. If it returns false, check your server's Redis service, firewall rules, and credentials.

Caches options and queries

The two biggest performance wins come from caching options (settings stored in the wp_options table) and post/term queries. Both are hit thousands of times per day.

Options caching is nearly automatic. WordPress calls get_option() constantly—for theme settings, plugin configuration, WooCommerce product attributes, and feature flags. With persistent object caching enabled, the first call loads from the database; subsequent calls within the cache TTL come from Redis. A typical WooCommerce site with 20+ options-heavy plugins sees 50-70% of option calls avoided.

Query caching requires more intentionality. You cache expensive queries explicitly:

php
// Without caching: hits the database every time
$posts = get_posts( array(
    'post_type'      => 'product',
    'posts_per_page' => 20,
    'orderby'        => 'meta_value_num',
    'meta_key'       => '_price',
) );

// With caching: check cache first
$cache_key = 'featured_products_price_sorted';
$posts = wp_cache_get( $cache_key, 'product_queries' );

if ( false === $posts ) {
    $posts = get_posts( array(
        'post_type'      => 'product',
        'posts_per_page' => 20,
        'orderby'        => 'meta_value_num',
        'meta_key'       => '_price',
    ) );
    wp_cache_set( $cache_key, $posts, 'product_queries', 3600 ); // 1 hour TTL
}

For taxonomies, WooCommerce product attributes, and user meta that rarely changes, a 1-2 hour TTL is safe. For trending or inventory-driven queries, keep it shorter—15-30 minutes—or invalidate on relevant hooks.

A real example from an e-commerce site: a "trending products" widget queries posts, calculates totals from meta, and sorts by sales. Without caching, that query runs 5-10ms per page load. With Redis, it's sub-millisecond after the first hit. On a site with 500 daily users, that's hundreds of milliseconds saved per user session.

Always invalidate cached queries when the underlying data changes:

php
// Invalidate on product update
add_action( 'save_post_product', function( $post_id ) {
    wp_cache_delete( 'featured_products_price_sorted', 'product_queries' );
} );

// Invalidate on meta change
add_action( 'updated_post_meta', function( $meta_id, $post_id, $meta_key ) {
    if ( '_price' === $meta_key ) {
        wp_cache_delete( 'featured_products_price_sorted', 'product_queries' );
    }
}, 10, 3 );

Huge win for logged-in users

Where Redis really shines is serving authenticated users. Unauthenticated visitors often hit page caches (like Cloudflare or Varnish), so they see cached HTML. Logged-in users—your paying customers, team members, and engaged readers—bypass page cache because their content is personalized.

A typical scenario: a user lands on their account dashboard. WordPress loads their user meta, purchase history, saved addresses, and profile settings. That's 10-20 database queries. With object caching, those queries stack in Redis after the first visit. Subsequent page loads see page-render times drop from 200ms to 50ms.

We measured this on a SaaS dashboard with ~1,000 daily active users. Peak database CPU dropped 40% after deploying Redis, and user-reported page latency fell from ~2 seconds to <400ms.

WooCommerce sites benefit even more. Cart operations, checkout flows, and my-account pages all hammer the database with user-specific queries. A single Redis instance can absorb those lookups without touching the DB, freeing database resources for write-heavy operations like order creation or inventory updates.

The trade-off: you must invalidate caches carefully. If a user updates their profile, you need to bust their cache immediately, not wait for a TTL expiry. Otherwise they'll see stale data:

php
// User profile update
add_action( 'profile_update', function( $user_id ) {
    wp_cache_delete( 'user_profile_' . $user_id, 'user_data' );
    wp_cache_delete( 'user_addresses_' . $user_id, 'user_data' );
} );

Monitor memory usage

Redis holds everything in RAM. If you cache indiscriminately, memory usage balloons and the server runs out of space, causing evictions or Redis crashes. Monitoring is non-negotiable.

Check Redis memory usage via CLI:

php
// Connect and check stats
redis-cli INFO memory

Look for the used_memory_human and maxmemory fields. If used memory approaches maxmemory, set an eviction policy and establish alerts.

Configure maxmemory and eviction in Redis:

php
// redis.conf
maxmemory 512mb
maxmemory-policy allkeys-lru

The allkeys-lru policy evicts least-recently-used keys when memory is full. This prevents crashes but can surprise developers if critical cached data is purged.

In production, we set maxmemory to 60-70% of available RAM and monitor weekly. A simple cron can export stats to your observability tool (Datadog, New Relic, CloudWatch):

php
// Monitor Redis memory via PHP
$redis = new Redis();
$redis->connect( '127.0.0.1', 6379 );
$info = $redis->info( 'memory' );

$used_mb = $info['used_memory'] / 1024 / 1024;
$max_mb = $info['maxmemory'] / 1024 / 1024;
$percent = ( $used_mb / $max_mb ) * 100;

// Log or send alert if > 80%
if ( $percent > 80 ) {
    error_log( "Redis memory usage: {$percent}%" );
}

Also track cache hit/miss ratios. Redis exposes these via INFO stats. A hit ratio below 50% suggests you're caching the wrong things or TTLs are too short.

Common mistakes

Caching user-specific data without proper invalidation. If you cache a user's order total and don't clear it when an order ships, they see stale data. Always invalidate on related hooks or use per-user cache keys.

Setting TTLs too long. A 24-hour TTL on WooCommerce product pricing means a price change takes 24 hours to reflect. Be conservative: 1-2 hours for mostly-stable data, 5-15 minutes for volatile content.

Caching large objects without considering serialization overhead. PHP serialization has a cost. Caching a 10MB array of posts might be slower than re-querying. Profile before and after.

Ignoring Redis memory limits. We've seen servers crash because Redis silently evicted keys when memory filled. Set alerts and review memory usage weekly.

Not accounting for cache warmup during deployments. If you flush Redis during a deploy, the first requests hit a cold cache, causing a traffic spike to the database. Warm up critical caches with a post-deploy script.

Key takeaways

  • Redis object caching is a drop-in plugin that persists the WordPress object cache across requests. Installation is simple; the payoff is significant.
  • Options and query results are the biggest wins. Lazy-load them on first request and cache aggressively, but invalidate on every relevant hook.
  • Logged-in users benefit most because they bypass page caches. A well-tuned object cache can cut their page load times by 50-80%.
  • Memory usage is your constraint. Set maxmemory, configure an eviction policy, and monitor weekly. A full Redis instance causes unpredictable performance.
  • Invest time in invalidation logic. A cache that returns stale data is worse than no cache. Always purge on updates, and use short TTLs for volatile content.
  • Measure hit/miss ratios and compare page times before and after. If you're not seeing 50%+ hit rates, re-examine what you're caching.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles