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:
User Request | vCheck Redis -----> HIT -----> Return cached data | MISS | vQuery Database | vStore in Redis | vReturn dataMy first implementation looked like this:
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?
Application Startup | vLoad ALL products into Redis | vUser Request | vQuery Redis directly -----> Return dataThe implementation would look like:
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:
| Aspect | Cache-Aside | Cache Prefetching |
|---|---|---|
| First request | Slow (cache miss) | Fast (pre-loaded) |
| Subsequent requests | Fast | Fast |
| Memory usage | Only what’s queried | Full dataset |
| Implementation complexity | Simpler | More complex (needs indexing) |
| Data freshness | Easier to manage | Must sync changes |
| Startup time | Instant | Depends on dataset size |
When to Use Cache-Aside
I realized cache-aside makes sense when:
-
Query patterns are unpredictable — You don’t know what users will search for. E-commerce with dynamic filters, user-specific data, ad-hoc reports.
-
Cost matters — You’re paying for Redis memory. Cache-aside keeps the footprint minimal. Perfect for cloud-hosted Redis.
-
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?
-
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:
-
Query patterns are known and limited — A product catalog with 100 SKUs, not 50,000. You know exactly what queries will come.
-
First-request latency is critical — Every millisecond counts. You can’t afford the initial cache miss penalty.
-
You have spare memory — Redis is cheap or already provisioned. The full dataset fits comfortably.
-
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:
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:
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 viableThe 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