Use this skill whenever the user mentions caching, cache, Redis, Upstash, cache invalidation, stale data, stale-while-revalidate, ISR strategy, CDN caching, edge caching, cache warming, cache stampede, thundering herd, 'data is stale', 'cache is wrong', 'when to cache', cache-control headers, TTL, cache busting, multi-layer cache, 'my data is outdated', or ANY caching strategy/design task — even if they don't explicitly say 'cache'. This skill designs coherent caching strategies across all layers.
Design a coherent caching strategy across all layers. This skill focuses on multi-layer architecture and cache invalidation. Cross-reference: vercel-power-user §3 for KV basics, §6 for ISR basics.
Your app has 5 cache layers. A request checks each layer from top to bottom:
Browser Cache → CDN Edge → Next.js Data Cache → Redis (Upstash) → Database
(client) (Vercel) (server) (app layer) (Supabase)
| Layer | TTL | Invalidation | Best For |
|---|---|---|---|
| Browser | seconds-hours | Cache-Control headers | Static assets, fonts |
| CDN Edge | seconds-days | s-maxage, deploy | Public pages, images |
| Next.js Data Cache | seconds-hours | revalidateTag, revalidatePath | fetch() results |
| Redis (Upstash) | seconds-days | Manual delete, TTL | API responses, computed data |
| Database | N/A | Source of truth | Everything |
// Server component — cached for 60 seconds
async function getAssessments() {
const res = await fetch(`${process.env.API_URL}/assessments`, {
next: { revalidate: 60 }, // Cache for 60 seconds
});
return res.json();
}
// Server component — cached with a tag
async function getOccupationList() {
const res = await fetch(`${process.env.API_URL}/occupations`, {
next: { tags: ["occupations"] }, // Tagged for invalidation
});
return res.json();
}
// Invalidate by tag when data changes
import { revalidateTag } from "next/cache";
revalidateTag("occupations"); // All fetches tagged "occupations" refetch
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Revalidate every hour
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const { secret, path, tag } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
}
if (tag) revalidateTag(tag);
if (path) revalidatePath(path);
return NextResponse.json({ revalidated: true });
}
npm install @upstash/redis
// lib/cache/redis.ts
import { Redis } from "@upstash/redis";
export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
// Read-through cache pattern
export async function cached<T>(
key: string,
fetcher: () => Promise<T>,
ttlSeconds = 3600
): Promise<T> {
const hit = await redis.get<T>(key);
if (hit !== null) return hit;
const data = await fetcher();
await redis.set(key, data, { ex: ttlSeconds });
return data;
}