---
name: advanced-caching-strategies
version: "1.0"
description: >
Multi-layer caching strategies across CDN, browser, server, and database.
PROACTIVELY activate for: (1) HTTP caching headers, (2) CDN configuration,
(3) Server-side caching with Redis, (4) Cache invalidation strategies, (5) ETag implementation.
Triggers: "caching", "cache strategy", "CDN", "browser cache", "server cache", "redis", "cache invalidation", "ETag", "Cache-Control"
core-integration:
techniques:
primary: ["systematic_analysis"]
secondary: ["structured_evaluation"]
contracts:
input: "none"
output: "none"
patterns: "none"
rubrics: "none"
---
# Advanced Caching Strategies
This skill provides guidance on designing and implementing multi-layered caching strategies across the full application stack (CDN, browser, server, database) to minimize latency, reduce server load, and improve overall application resilience and user experience.
## The Cache Hierarchy
Caching can be implemented at multiple layers, from closest to the user to closest to the data source:
1. **Browser Cache**: Client-side caching (HTTP caching, LocalStorage, IndexedDB)
2. **CDN Edge Cache**: Geographically distributed caching (Vercel Edge, CloudFront, Cloudflare)
3. **Server-Side Cache**: Application-level caching (Redis, in-memory)
4. **Database Cache**: Query result caching, connection pooling
**Goal**: Serve content from the closest, fastest cache layer possible.
## Browser Caching
Browser caching reduces network requests by storing resources locally.
### HTTP Cache-Control Headers
The `Cache-Control` header controls how and for how long browsers cache responses.
#### Immutable Static Assets
```http
# For assets with content-addressed filenames (e.g., app.abc123.js)
Cache-Control: public, max-age=31536000, immutable
```
- **public**: Can be cached by browsers and CDNs
- **max-age=31536000**: Cache for 1 year (in seconds)
- **immutable**: Never revalidate (file name changes when content changes)
**Next.js**: Automatically sets this for `/_next/static/` files
#### Dynamic HTML Pages
```http
# For HTML pages that change periodically
Cache-Control: public, max-age=0, must-revalidate
```
- **max-age=0**: Revalidate on every request
- **must-revalidate**: Must check server before using stale cache
Or with stale-while-revalidate:
```http
Cache-Control: public, max-age=60, stale-while-revalidate=86400
```
- Serve from cache for 60s
- If 60s-24h old, serve stale content while revalidating in background
#### API Responses
```http
# User-specific data (don't cache)
Cache-Control: private, no-store
# Public data that changes occasionally
Cache-Control: public, max-age=300, stale-while-revalidate=600
```
- **private**: Only browser cache (not CDN)
- **no-store**: Don't cache at all
- **stale-while-revalidate**: Serve stale data while fetching fresh data
### ETag and Conditional Requests
ETags enable efficient revalidation without re-downloading unchanged content.
#### Server Implementation (Next.js API Route)
```ts
// app/api/data/route.ts
import { NextResponse } from 'next/server'
import crypto from 'crypto'
export async function GET(request: Request) {
const data = await fetchData()
const content = JSON.stringify(data)
// Generate ETag from content hash
const etag = crypto.createHash('md5').update(content).digest('hex')
// Check If-None-Match header
const clientEtag = request.headers.get('if-none-match')
if (clientEtag === etag) {
// Content hasn't changed
return new NextResponse(null, { status: 304 })
}
// Content changed, send full response
return NextResponse.json(data, {
headers: {
'ETag': etag,
'Cache-Control': 'public, max-age=300',
},
})
}
```
#### Python FastAPI Implementation
```python
from fastapi import FastAPI, Request, Response
import hashlib
import json
app = FastAPI()
@app.get("/api/data")
async def get_data(request: Request):
data = await fetch_data()
content = json.dumps(data)
# Generate ETag
etag = hashlib.md5(content.encode()).hexdigest()
# Check If-None-Match
if request.headers.get("if-none-match") == etag:
return Response(status_code=304)
# Return with ETag
return Response(
content=content,
headers={
"ETag": etag,
"Cache-Control": "public, max-age=300"
}
)
```
### LocalStorage and IndexedDB
For application state and data that doesn't need to be in HTTP cache.
```ts
// Simple cache wrapper for localStorage
class BrowserCache {
static set(key: string, value: any, ttlMs: number) {
const item = {
value,
expiry: Date.now() + ttlMs,
}
localStorage.setItem(key, JSON.stringify(item))
}
static get(key: string) {
const itemStr = localStorage.getItem(key)
if (!itemStr) return null
const item = JSON.parse(itemStr)
if (Date.now() > item.expiry) {
localStorage.removeItem(key)
return null
}
return item.value
}
}
// Usage
BrowserCache.set('user-prefs', preferences, 7 * 24 * 60 * 60 * 1000) // 7 days
const prefs = BrowserCache.get('user-prefs')
```
## CDN Caching
CDNs cache content at edge locations close to users, reducing latency and server load.
### Vercel Edge Network (Next.js)
Vercel automatically caches static assets and certain dynamic routes.
```ts
// app/products/page.tsx
export const revalidate = 3600 // Cache for 1 hour
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }
}).then(r => r.json())
return
}
```
### Custom CDN Headers
```ts
// app/api/public-data/route.ts
export async function GET() {
const data = await fetchPublicData()
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
'CDN-Cache-Control': 'max-age=7200',
},
})
}
```
- **s-maxage**: CDN cache duration (overrides max-age for shared caches)
- **CDN-Cache-Control**: Cloudflare-specific directive
### Cache Key Configuration
Ensure cache keys include relevant parameters:
```ts
// BAD: Same cache for all users
fetch(`https://api.example.com/dashboard`)
// GOOD: User-specific cache key
fetch(`https://api.example.com/dashboard`, {
headers: {
'x-user-id': userId,
},
cache: 'no-store', // Don't cache user-specific data in CDN
})
```
### CDN Purging
```ts
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const { path, tag } = await request.json()
if (path) {
revalidatePath(path) // Purge specific path
}
if (tag) {
revalidateTag(tag) // Purge all fetches with this tag
}
return Response.json({ revalidated: true })
}
```
## Server-Side Caching
Application-level caching reduces database load and improves response times.
### Next.js React cache()
```ts
// lib/data.ts
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
// This function is memoized during a single request
const user = await db.query('SELECT * FROM users WHERE id = ?', [id])
return user
})
// Can be called multiple times in components without re-fetching
const user1 = await getUser('123')
const user2 = await getUser('123') // Returns memoized result
```
### Next.js unstable_cache
```ts
import { unstable_cache } from 'next/cache'
export const getCachedProducts = unstable_cache(
async () => {
return await db.query('SELECT * FROM products')
},
['products-list'], // Cache key
{
revalidate: 3600, // Cache for 1 hour
tags: ['products'], // For on-demand revalidation
}
)
```
### Redis Caching (Python)
```python
import redis
import json
from functools import wraps
redis_client = redis.Redis(host='localhost', port=6379, db=0)
def cache_result(ttl: int = 300):
"""Decorator for caching function results in Redis"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# Generate cache key from function name and arguments
cache_key = f"{func.__name__}:{args}:{kwargs}"
# Try to get from cache
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# Cache miss - execute function
result = await func(*args, **kwargs)
# Store in cache
redis_client.setex(
cache_key,
ttl,
json.dumps(result)
)
return result
return wrapper
return decorator
# Usage
@cache_result(ttl=600)
async def get_user_profile(user_id: str):
return await db.fetch_one(
"SELECT * FROM users WHERE id = ?",
user_id
)
```
### LRU Cache (Python)
For single-process applications:
```python
from functools import lru_cache
@lru_cache(maxsize=1000)
def get_exchange_rate(from_currency: str, to_currency: str) -> float:
"""Cache recent exchange rate lookups"""
response = requests.get(
f"https://api.example.com/rate/{from_currency}/{to_currency}"
)
return response.json()['rate']
# Clear cache when needed
get_exchange_rate.cache_clear()
```
## Database Caching
Optimize database performance through caching and connection pooling.
### Query Result Caching
```python
# Using Redis for query result caching
async def get_popular_products():
cache_key = "popular_products"
# Check cache
cached = await redis_client.get(cache_key)
if cached:
return json.loads(cached)
# Cache miss - query database
products = await db.fetch_all(
"""
SELECT * FROM products
WHERE views > 1000
ORDER BY views DESC
LIMIT 20
"""
)
# Cache for 10 minutes
await redis_client.setex(
cache_key,
600,
json.dumps(products)
)
return products
```
### Connection Pooling
```python
# PostgreSQL connection pool
import asyncpg
pool = await asyncpg.create_pool(
host='localhost',
database='mydb',
user='user',
password='password',
min_size=10, # Minimum connections
max_size=20, # Maximum connections
)
# Use pooled connection
async with pool.acquire() as connection:
result = await connection.fetch('SELECT * FROM users')
```
### Prepared Statements
```python
# Reuse parsed queries
async with pool.acquire() as conn:
stmt = await conn.prepare('SELECT * FROM users WHERE id = $1')
# Execute multiple times without re-parsing
user1 = await stmt.fetchrow(123)
user2 = await stmt.fetchrow(456)
```
## Cache Invalidation Strategies
"There are only two hard things in Computer Science: cache invalidation and naming things."
### Time-Based (TTL)
Simplest strategy: data expires after a set time.
```ts
// Cache for 5 minutes
fetch('https://api.example.com/data', {
next: { revalidate: 300 }
})
```
**Pros**: Simple, predictable
**Cons**: Data can be stale for up to TTL duration
### On-Demand Invalidation
Invalidate cache when data changes.
```ts
// When product is updated
await fetch('/api/revalidate', {
method: 'POST',
body: JSON.stringify({ tag: 'products' }),
})
```
**Pros**: Always fresh data
**Cons**: Requires webhook/trigger on every update
### Stale-While-Revalidate
Serve stale content while fetching fresh data in the background.
```http
Cache-Control: max-age=60, stale-while-revalidate=86400
```
**Pros**: Fast response (always from cache), eventually consistent
**Cons**: Users may see stale data briefly
### Write-Through Cache
Update cache atomically with database write.
```python
async def update_user(user_id: str, data: dict):
# Update database
await db.execute(
"UPDATE users SET name = $1 WHERE id = $2",
data['name'],
user_id
)
# Update cache
cache_key = f"user:{user_id}"
await redis_client.setex(
cache_key,
3600,
json.dumps(data)
)
```
**Pros**: Cache always consistent with database
**Cons**: Slower writes (two operations)
### Cache-Aside Pattern
Application checks cache, fetches from DB on miss, then populates cache.
```python
async def get_user(user_id: str):
cache_key = f"user:{user_id}"
# Check cache
cached = await redis_client.get(cache_key)
if cached:
return json.loads(cached)
# Cache miss - fetch from DB
user = await db.fetch_one(
"SELECT * FROM users WHERE id = $1",
user_id
)
# Populate cache
await redis_client.setex(cache_key, 3600, json.dumps(user))
return user
```
**Pros**: Only caches accessed data
**Cons**: Cache can become stale if DB updated elsewhere
## Anti-Patterns
### Caching User-Specific Data in Public Cache
```http
# BAD: User data cached in CDN
Cache-Control: public, max-age=3600
```
**Result**: User A sees User B's data
**Fix**: Use `private` or `no-store` for user-specific data
### No Cache-Busting for Static Assets
```html
```
**Next.js handles this automatically** for `/_next/static/` files.
### Long TTL Without Invalidation Strategy
```ts
// BAD: Data could be stale for a month
fetch(url, { next: { revalidate: 2592000 } })
```
**Fix**: Use shorter TTL or implement on-demand invalidation.
### Not Varying Cache by Request Headers
```ts
// BAD: Same cache for all languages
fetch('/api/content')
// GOOD: Cache varies by Accept-Language
fetch('/api/content', {
headers: {
'Accept-Language': locale,
},
})
```
### Ignoring Cache Stampede
Multiple requests fetch same data simultaneously when cache expires (thundering herd).
**Fix**: Use request coalescing or stale-while-revalidate.
## Caching Strategy Decision Tree
```
Is the data user-specific?
├─ YES → Use private cache or no-store
│ └─ High traffic? → Server-side cache (Redis) with user-specific keys
└─ NO → Is the data static?
├─ YES → Cache-Control: public, max-age=31536000, immutable
└─ NO → How often does it change?
├─ Frequently (< 1 min) → Cache-Control: max-age=60, stale-while-revalidate
├─ Periodically (< 1 hour) → Cache-Control: max-age=300 + on-demand invalidation
└─ Rarely (> 1 hour) → Cache-Control: max-age=3600
```
## Caching Checklist
- [ ] Static assets cached with long TTL and immutable flag
- [ ] HTML pages use stale-while-revalidate pattern
- [ ] User-specific data NOT cached in CDN (use private/no-store)
- [ ] ETags implemented for efficient revalidation
- [ ] CDN configured for public cacheable routes
- [ ] Redis/in-memory cache for hot database queries
- [ ] Database connection pooling configured
- [ ] Cache invalidation strategy defined and implemented
- [ ] Cache keys include relevant parameters (avoid cache poisoning)
- [ ] Monitoring for cache hit rates and effectiveness
## Performance Impact
Effective caching can:
- **Reduce server load**: 70-90% reduction for cacheable content
- **Improve response time**: 10-100x faster (edge cache vs origin)
- **Reduce database load**: 80-95% reduction for popular queries
- **Improve reliability**: Serve stale content during outages