Opinionated rules for implementing multi-layer caching (browser, CDN/edge, application, database) to maximise web-app performance, resilience and correctness.
You know the feeling—your carefully crafted API is lightning-fast in development, but production users are complaining about slow page loads and your database is melting under traffic spikes. The problem isn't your code architecture; it's that you're hitting the database for data that could be served from cache layers you're not even using yet.
Most developers implement caching as an afterthought, slapping Redis onto an existing architecture without considering the full spectrum of cache layers available. This leads to:
The issue isn't just about adding a cache—it's about building a coherent multi-layer strategy that handles the entire request lifecycle from browser to database.
These Cursor Rules implement a battle-tested caching hierarchy that automatically routes requests through the fastest available layer while maintaining data consistency. Instead of one-size-fits-all caching, you get intelligent cache selection based on data freshness requirements and request patterns.
The four-layer approach:
Each layer has specific rules for what data belongs there, how long it stays fresh, and when to invalidate.
Stop Database Overload Your database queries drop by 70-90% because the rules implement aggressive query result caching with smart invalidation. API endpoints that used to hit the database every time now serve from Redis with microsecond response times.
Eliminate Cache Stampedes The Stale-While-Revalidate pattern means your app serves cached data immediately while refreshing in the background. No more thundering herd problems when popular cache keys expire.
Reduce CDN Costs Intelligent browser caching means fewer requests hit your CDN. Static assets get versioned URLs for permanent caching, while dynamic content uses optimal TTL strategies.
Improve User Experience Page load times become consistently fast because the rules implement Service Worker patterns that work offline and serve instant responses for repeat visits.
API Response Optimization
// Before: Every request hits your database
app.get('/api/products/:id', async (req, res) => {
const product = await db.products.findById(req.params.id);
res.json(product);
});
// After: Multi-layer caching with automatic SWR
app.get('/api/products/:id', async (req, res) => {
const cacheKey = `product:${req.params.id}:v${process.env.APP_VERSION}`;
// Application layer cache check
let product = await cache.get(cacheKey);
if (!product) {
product = await db.products.findById(req.params.id);
await cache.set(cacheKey, product, { ttl: 300, swr: 86400 });
}
res.set('Cache-Control', 'max-age=300, stale-while-revalidate=86400');
res.json(product);
});
Static Asset Performance Instead of cache-busting nightmares, you get immutable asset URLs:
<!-- Cacheable forever because URL changes with content -->
<link rel="stylesheet" href="/static/v1.2.3/main.css">
<script src="/static/v1.2.3/app.js"></script>
Service Worker Intelligence
// Cache strategy that adapts to content type
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) {
// Network first with SWR fallback for API calls
event.respondWith(networkFirstStrategy(event.request));
} else if (event.request.url.includes('/static/')) {
// Cache first for versioned static assets
event.respondWith(cacheFirstStrategy(event.request));
}
});
Framework Integration Examples
Next.js with Intelligent Revalidation:
export async function getStaticProps({ params }) {
return {
props: { data: await fetchData(params.id) },
revalidate: 300, // SWR every 5 minutes
};
}
Laravel with Tagged Invalidation:
Route::get('/products/{id}', function ($id) {
return Cache::tags(['products', "product:{$id}"])
->remember("product:{$id}", 300, function () use ($id) {
return Product::findOrFail($id);
});
});
Step 1: Set Up Your Cache Layers
Install the core dependencies:
npm install redis quick-lru
# or
pip install django-redis
# or
composer require predis/predis
Step 2: Configure Your Primary Cache
For Node.js applications:
import Redis from 'redis';
import LRU from 'quick-lru';
// External cache (Redis)
const redis = Redis.createClient({
host: process.env.REDIS_HOST,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
});
// In-memory cache (don't exceed 70% of heap)
const localCache = new LRU({
maxSize: 10_000,
ttl: 600_000 // 10 minutes
});
Step 3: Implement Cache-First Logic
async function getCachedData(key, fetcher, options = {}) {
const { ttl = 300, swr = 86400 } = options;
// Try local cache first
let data = localCache.get(key);
if (data) return data;
// Try Redis with circuit breaker
try {
data = await redis.get(key);
if (data) {
localCache.set(key, data);
return JSON.parse(data);
}
} catch (error) {
console.warn('Redis unavailable, falling back to database');
}
// Fetch from source
data = await fetcher();
// Store in both layers
localCache.set(key, data);
redis.setex(key, ttl, JSON.stringify(data)).catch(console.warn);
return data;
}
Step 4: Add HTTP Caching Headers
function setCacheHeaders(res, { maxAge = 300, swr = 86400, private = false }) {
const visibility = private ? 'private' : 'public';
res.set('Cache-Control', `${visibility}, max-age=${maxAge}, stale-while-revalidate=${swr}`);
res.set('Vary', 'Accept-Encoding, Accept-Language');
}
Step 5: Monitor Cache Performance
function logCacheStatus(key, status, layer) {
console.log({
cache_key: key,
cache_status: status, // hit|miss|stale|bypass
cache_layer: layer, // browser|edge|app|db
timestamp: new Date().toISOString(),
});
}
Immediate Performance Gains:
Resilience Improvements:
Developer Experience:
Measurable Targets:
These rules transform caching from a configuration headache into a performance multiplier. Your database stops being the bottleneck, your users get consistently fast experiences, and your infrastructure costs drop while handling more traffic.
The best part? The rules handle the complexity automatically—you write normal application code, and the caching layers optimize everything behind the scenes.
You are an expert in HTTP caching, CDN/edge networks (Cloudflare, Akamai), service-worker APIs, Redis/Memcached, and framework-level caches in Next.js, Remix, Laravel, Django and Spring.
Key Principles
- Think “data freshness vs. speed” for every request. Pick the lowest-latency cache layer that still guarantees required consistency.
- Favour immutable caching with versioned URLs; invalidate only when absolutely necessary.
- All cache keys must be deterministic, environment-safe strings. Include version, locale, tenant or device when they affect the payload.
- Prefer Stale-While-Revalidate (SWR) over “hard” purges to avoid thundering-herd effects.
- Log and surface cache-hit/miss ratios; treat <85 % hit rates as performance bugs.
- Never cache personal or sensitive data in shared caches (CDNs, proxy). Use the “private” or “no-store” directives accordingly.
- Add monitoring alerts for sudden TTL spikes or stampedes.
JavaScript / TypeScript
- Use fetch()/Axios response interceptor to add custom Cache-Control on server-side fetches: `max-age=300, stale-while-revalidate=86400`.
- Always include a `version` (git sha or build number) in static asset URLs (`/static/${version}/main.css`) to make them effectively immutable.
- Service Worker:
• Cache First for static assets (icon fonts, CSS).
• Network First with SWR fallback for dynamic API calls.
• Use IndexedDB for >5 MB datasets; otherwise Cache API.
- Node.js in-memory LRU example using `quick-lru`:
```ts
import LRU from 'quick-lru';
export const pageCache = new LRU<string, string>({ maxSize: 10_000, ttl: 600_000 });
```
- Do not exceed 70 % of Node heap with in-process caches; switch to Redis cluster when approaching the limit.
Python (Django)
- Wrap expensive queryset calls with `@cache_page(60*15)` but supply `key_prefix` using site+language to avoid collisions.
- Use `django-redis` backend; set `IGNORE_EXCEPTIONS = True` so Redis outage doesn’t take site down.
Go
- Store HTTP ETag in memory map protected by `sync.RWMutex`; rebuild only when content digest changes.
- When using `groupcache`, set `MainCacheBytes` ≤ 50 % of container memory to leave headroom.
Error Handling and Validation
- Detect stampede: if cache miss + mutex locked + >N waiters, immediately return stale copy (if available) and trigger async rebuild.
- Surround all Redis/Memcached calls with circuit breaker (e.g., opossum) to fail open.
- On validation failure (ETag mismatch, checksum error), purge the singular key rather than full namespace.
- Include fallback path for each cache layer: Browser → CDN → App-level → DB. Log which layer returned data.
Framework-Specific Rules
Next.js / Remix
- Use `revalidate` property in `getStaticProps` / `loader` to implement SWR: `export const revalidate = 300`.
- Route Segment Caching: group routes with similar TTL to minimise invalidations.
Laravel
- Apply `Route::cache()` middleware; use `tags()` for multi-tenant invalidation.
- Queue cache warm-up jobs after deployment to prime Redis.
Django
- Enable `TEMPLATE_CACHE` for fragment caching. Namespaced by site + language.
- For API views, return `ConditionalGetMixin` to expose ETag/Last-Modified.
Spring
- Annotate heavy methods with `@Cacheable(value="product", key="#id", unless="#result.outOfStock")`.
- Combine with Caffeine local cache + Redis external.
Additional Sections
Testing
- Use k6 or Artillery to simulate 10× traffic with cold, warm and hot cache scenarios. Aim for <5 % aperture increase between warm and hot.
- Unit-test cache key generators: given same semantic input they must return identical keys; otherwise treat as bug.
Performance & Monitoring
- Emit `cache_status` (hit|miss|stale|bypass) and `cache_layer` (browser|edge|app|db) in every log line.
- Promote Grafana panels for 95th-percentile latency before/after cache.
Security
- Default to `Cache-Control: no-store,private` for all authenticated responses.
- Strip `Set-Cookie` header when caching at CDN unless using request coalescing.
- Enable signed URLs with short expiry for premium private assets.