Skip to content

Cache-Aside vs Cache Prefetching: Which Redis Caching Pattern Should You Use?

I was building a product catalog service for an e-commerce platform. The database was getting hammered with the same queries over and over. Obviously, I needed Redis caching. But here’s the thing — I couldn’t decide between two approaches: cache-aside (lazy loading) or cache prefetching.

The documentation said both work. The tutorials showed both patterns. But nobody explained when to use which one. So I dug deeper, tested both, and here’s what I learned.

The Problem: Two Patterns, No Clear Answer

My service had a products table with about 50,000 SKUs. Users could search by category, brand, price range, and various filters. Some filters were common (category, brand), others were rare (specific color codes, obscure attributes).

I started with the naive approach: query the database every time. Predictable result — slow responses, high DB CPU, and angry users during traffic spikes.

Redis was the obvious fix. But which pattern?

Cache-Aside: Fill On Demand

Cache-aside (also called lazy loading) populates the cache only when data is requested. Here’s the flow:

Cache-Aside Flow
User Request
|
v
Check Redis -----> HIT -----> Return cached data
|
MISS
|
v
Query Database
|
v
Store in Redis
|
v
Return data

My first implementation looked like this:

cache-aside.js
async function getProducts(filters) {
const cacheKey = `products:${hashFilters(filters)}`;
// Check cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Cache miss - query database
const results = await db.query(
'SELECT * FROM products WHERE category = ? AND brand = ?',
[filters.category, filters.brand]
);
// Store in cache with TTL
await redis.set(cacheKey, JSON.stringify(results), 'EX', 3600);
return results;
}

This worked well for most queries. But I noticed something — the first request for any filter combination was slow. The cache miss required a database round-trip.

For my use case, this meant:

  • Popular queries (iPhone, Samsung) were lightning fast after the first hit
  • Niche queries (specific vintage camera model) were slow on first request, but who cared? Those were rare.

The cache size stayed manageable. I was only storing what people actually queried.

Cache Prefetching: Load Everything Upfront

Then I considered the alternative: what if I pre-loaded everything?

Cache Prefetching Flow
Application Startup
|
v
Load ALL products into Redis
|
v
User Request
|
v
Query Redis directly -----> Return data

The implementation would look like:

prefetch.js
async function initializeCache() {
// Load all products on startup
const allProducts = await db.query('SELECT * FROM products');
for (const product of allProducts) {
await redis.json.set(`product:${product.id}`, '$', product);
}
// Create search indexes
await redis.ft.create('products_idx', {
'$.category': { type: 'TEXT' },
'$.brand': { type: 'TEXT' },
'$.price': { type: 'NUMERIC' }
});
}
async function getProducts(filters) {
// Query Redis directly - no database involved
return await redis.ft.search('products_idx',
`@category:${filters.category} @brand:${filters.brand}`
);
}

Every query would be fast from the start. No cache misses. But I had concerns:

  • Memory usage: 50,000 products in Redis
  • Startup time: How long to load everything?
  • Staleness: What happens when products change?

The Comparison

I built a quick test to compare both approaches:

AspectCache-AsideCache Prefetching
First requestSlow (cache miss)Fast (pre-loaded)
Subsequent requestsFastFast
Memory usageOnly what’s queriedFull dataset
Implementation complexitySimplerMore complex (needs indexing)
Data freshnessEasier to manageMust sync changes
Startup timeInstantDepends on dataset size

When to Use Cache-Aside

I realized cache-aside makes sense when:

  1. Query patterns are unpredictable — You don’t know what users will search for. E-commerce with dynamic filters, user-specific data, ad-hoc reports.

  2. Cost matters — You’re paying for Redis memory. Cache-aside keeps the footprint minimal. Perfect for cloud-hosted Redis.

  3. Dataset is large but access is sparse — 50,000 products, but only 5,000 get 90% of the traffic. Why pay for the other 45,000 in cache?

  4. Data changes frequently — Write-through or write-behind is easier to implement with cache-aside.

For my product catalog, this hit the nail on the head. Most users searched for popular brands. Niche searches were rare. Cache-aside was the winner.

When to Use Cache Prefetching

But prefetching has its place. Consider it when:

  1. Query patterns are known and limited — A product catalog with 100 SKUs, not 50,000. You know exactly what queries will come.

  2. First-request latency is critical — Every millisecond counts. You can’t afford the initial cache miss penalty.

  3. You have spare memory — Redis is cheap or already provisioned. The full dataset fits comfortably.

  4. Data changes rarely — Product catalog updates once a day, not every minute. Sync overhead is minimal.

I could see this working for a smaller boutique store. Fifty products total. Known categories. Pre-load everything, enjoy instant responses.

The Hybrid Approach

Here’s what I ended up doing — a hybrid:

hybrid.js
async function initializeCache() {
// Pre-cache top 100 most searched products
const hotProducts = await db.query(`
SELECT p.* FROM products p
JOIN search_logs s ON p.id = s.product_id
GROUP BY p.id
ORDER BY COUNT(*) DESC
LIMIT 100
`);
for (const product of hotProducts) {
await redis.json.set(`product:${product.id}`, '$', product);
}
}
async function getProducts(filters) {
const cacheKey = `products:${hashFilters(filters)}`;
// Try cache first (might be pre-loaded or lazy-loaded)
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// Cache miss - lazy load
const results = await db.query(/* ... */);
await redis.set(cacheKey, JSON.stringify(results), 'EX', 3600);
return results;
}

Pre-cache the hot items (maybe 5% of queries), use cache-aside for the rest. Best of both worlds.

Common Mistakes I Almost Made

Mistake 1: Pre-caching everything because “more cache is better”

For my 50,000 product catalog, this would have meant:

  • ~500MB of Redis memory (assuming ~10KB per product JSON)
  • Long startup time on each deployment
  • Complex sync logic when products updated

Instead, I measured actual query patterns and found 90% of traffic hit 10% of products. Cache-aside with smart TTL was more efficient.

Mistake 2: Using cache-aside for a tiny catalog

If I only had 100 products, prefetching would have been simpler. No cache miss penalty, trivial memory cost, straightforward implementation. Sometimes the “lazy” approach isn’t worth the complexity.

Mistake 3: Forgetting cache invalidation

With prefetching, I would have needed to update Redis every time a product price changed. With cache-aside, I could just delete the cache key and let the next query re-populate it. Much simpler.

Decision Framework

Here’s the mental model I use now:

Decision Tree
Is your dataset small (<1000 items)?
└── YES → Consider prefetching
└── NO → Do you know exactly what queries to expect?
└── YES → Consider prefetching for those known queries
└── NO → Use cache-aside
Is first-request latency critical?
└── YES → Prefetch hot items, cache-aside the rest (hybrid)
└── NO → Cache-aside is sufficient
Does data change frequently?
└── YES → Cache-aside (easier invalidation)
└── NO → Either works, prefetching more viable

The Bottom Line

For most growing applications with unpredictable query patterns, cache-aside provides the best balance of cost, simplicity, and performance. You pay only for what you use, and the implementation is straightforward.

But don’t blindly follow the lazy loading pattern. If you have a small, predictable dataset where every query should be instant, prefetching saves you from those first-request cache misses.

The right answer isn’t one or the other — it’s understanding your data, your users, and your constraints. Measure first, cache second.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments