Redis Caching — Using Redis for SSR Cache Storage
In this tutorial, you will learn about Redis Caching. We cover key concepts, practical examples, and best practices to help you master this topic.
Redis caching for SSR stores rendered HTML fragments and full pages in memory, providing sub-millisecond read times, TTL-based expiration, cache invalidation patterns, and high availability for production SSR applications.
What You'll Learn
By the end of this tutorial, you will understand how to use Redis as an SSR cache store, implement cache-aside and write-through patterns, handle cache invalidation with Redis keys and patterns, manage cache TTLs, and configure Redis for high availability in SSR applications.
Why It Matters
SSR without Redis caching does not scale. Each request renders the same components and fetches the same data. Redis stores rendered HTML in memory, serving cached responses in 1-5ms instead of 100-500ms render times. For high-traffic SSR applications, Redis caching is essential for performance and cost.
Real-World Use
A news site serving 1 million page views per day used Redis caching for SSR. Each uncached page took 450ms to render. With Redis, cached pages served in 3ms. They reduced their server fleet from 20 to 4 instances, saving 80 percent on hosting costs while maintaining fast response times.
Redis Cache Patterns for SSR
┌──────────────────────────────────────────────────────────┐
│ Redis Cache Patterns for SSR │
├──────────────────────────────────────────────────────────┤
│ │
│ Cache-Aside (Lazy Loading): │
│ 1. Request comes in │
│ 2. Check Redis for cached HTML │
│ 3. If found → return cached HTML │
│ 4. If not found → render page │
│ 5. Store rendered HTML in Redis │
│ 6. Return HTML to client │
│ │
│ Write-Through: │
│ 1. Content is created/updated │
│ 2. Render the page │
│ 3. Store in Redis immediately │
│ 4. First request gets cached version │
│ │
│ Cache Invalidation: │
│ 1. Content changes │
│ 2. Delete or update related cache keys │
│ 3. Next request renders fresh page │
│ 4. Cache is repopulated │
│ │
└──────────────────────────────────────────────────────────┘
Think of Redis caching for SSR like a chef's mise en place. Before service begins, the chef preps ingredients (renders pages) and stores them in labeled containers (Redis keys). When an order comes in (request), the chef grabs the prepped ingredients (cached HTML) and assembles the dish in seconds instead of cooking from scratch.
Redis Cache Implementation
const { createClient } = require('redis');
const { renderToString } = require('react-dom/server');
class SSRCache {
constructor() {
this.client = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
socket: {
reconnectStrategy: (retries) => Math.min(retries * 50, 1000)
}
});
this.client.on('error', (err) => {
console.error('Redis error:', err);
});
this.client.connect();
this.defaultTTL = 300; // 5 minutes
}
// Cache-aside pattern
async getOrRender(key, renderFn, ttl = this.defaultTTL) {
try {
// 1. Try cache
const cached = await this.client.get(key);
if (cached) {
console.log(`Redis HIT: ${key}`);
return { html: cached, source: 'cache' };
}
// 2. Cache miss — render
console.log(`Redis MISS: ${key}`);
const html = await renderFn();
// 3. Store in cache (don't await — fire and forget)
this.client.setEx(key, ttl, html).catch(err => {
console.error('Redis set error:', err);
});
return { html, source: 'render' };
} catch (error) {
console.error('Cache error:', error);
// Fallback to rendering
const html = await renderFn();
return { html, source: 'render' };
}
}
// Invalidate single key
async invalidate(key) {
await this.client.del(key);
console.log(`Invalidated: ${key}`);
}
// Invalidate by pattern
async invalidatePattern(pattern) {
const keys = await this.client.keys(`ssr:${pattern}:*`);
if (keys.length > 0) {
await this.client.del(keys);
console.log(`Invalidated ${keys.length} keys: ${pattern}`);
}
}
// Tag-based invalidation
async invalidateByTag(tag) {
const keys = await this.client.sMembers(`tag:${tag}`);
if (keys.length > 0) {
await this.client.del(keys);
await this.client.del(`tag:${tag}`);
console.log(`Invalidated ${keys.length} keys with tag: ${tag}`);
}
}
// Tag a cache key for group invalidation
async tagKey(key, tags) {
for (const tag of tags) {
await this.client.sAdd(`tag:${tag}`, key);
}
}
}
module.exports = new SSRCache();
Cache Invalidation Strategies
const cache = require('./ssrCache');
// On content update — invalidate related caches
app.post('/api/posts', async (req, res) => {
const post = await db.posts.create(req.body);
// Invalidate:
// 1. Home page (shows latest posts)
await cache.invalidate('ssr:/');
// 2. Category pages
await cache.invalidatePattern(`ssr:/category:${post.category}`);
// 3. RSS feed
await cache.invalidate('ssr:/feed.xml');
// 4. Tag-based: invalidate all pages tagged with this post's category
await cache.invalidateByTag(`category:${post.category}`);
console.log('Cache invalidated for new post:', post.id);
res.redirect(303, `/posts/${post.slug}`);
});
// Update product — invalidate product page and listing
app.put('/api/products/:id', async (req, res) => {
const product = await db.products.update(req.params.id, req.body);
// Invalidate product detail page
await cache.invalidate(`ssr:/products/${product.slug}`);
// Invalidate product listing pages
await cache.invalidatePattern('ssr:/products');
await cache.invalidatePattern('ssr:/category:');
// Re-render and cache the product page immediately (write-through)
const html = await renderToString(
React.createElement(ProductPage, { product })
);
await cache.client.setEx(
`ssr:/products/${product.slug}`,
cache.defaultTTL,
html
);
res.json({ success: true });
});
Common Mistakes
- No Redis persistence. By default, Redis stores data in memory. Without persistence (RDB/AOF), all cached HTML is lost on restart. Enable persistence for production.
- Cache key collisions. Different URLs that map to the same cache key cause content mixing. Include the full URL, query parameters, and language/locale in the cache key.
- Over-caching with too long TTL. Long TTLs serve stale content. Short TTLs increase server load. Balance based on how often content changes.
- Not handling Redis failures gracefully. If Redis is down, your application should still work (fallback to rendering). Implement circuit breakers and fallbacks.
- Large cache values. Redis works best with values under 10MB. Large HTML pages should be compressed (gzip) before storing in Redis.
Practice Questions
- What is the cache-aside pattern and how does it work with Redis?
- How do you implement cache invalidation by tag in Redis?
- Why should you compress HTML before storing in Redis?
- How do you handle Redis connection failures in production?
- What is the difference between invalidate and invalidatePattern?
Challenge: Implement a complete Redis caching layer for an SSR application: cache-aside pattern for all public pages, tag-based invalidation (tag pages by category and author), write-through caching for frequently accessed content, gzip compression for cached HTML, Redis persistence configuration (RDB), and graceful fallback when Redis is unavailable.
FAQ
Mini Project
Set up Redis caching for an SSR blog: Redis cache-aside for all public pages with 10-minute TTL, tag-based invalidation by category and author, gzip compression of cached HTML, Redis persistence with AOF, cache warmup script that crawls the sitemap on deploy, and a dashboard showing cache hit ratio, memory usage, and TTFB improvement.
What's Next
You understand Redis caching. Now explore SSR Performance to optimize overall SSR rendering speed.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro