Caching Strategies
Patterns for application caching to improve performance and reduce load.
Overview#
Effective caching dramatically improves application performance. This pattern covers:
- React cache for request deduplication
- Next.js data cache
- Redis caching
- Stale-while-revalidate pattern
- Memoization utilities
Prerequisites#
npm install @upstash/redisCode Example#
React Cache (Request Deduplication)#
1// lib/cache.ts
2import { cache } from 'react'
3import { prisma } from '@/lib/db'
4
5// React cache - deduplicates within a single request
6export const getUser = cache(async (userId: string) => {
7 return prisma.user.findUnique({ where: { id: userId } })
8})
9
10// Multiple calls within same request = 1 database query
11export async function UserProfile({ userId }: { userId: string }) {
12 const user = await getUser(userId) // First call - hits DB
13 return <Profile user={user} />
14}
15
16export async function UserSidebar({ userId }: { userId: string }) {
17 const user = await getUser(userId) // Same request - cached
18 return <Sidebar user={user} />
19}Next.js Data Cache#
1// Fetch with caching
2async function getData() {
3 const res = await fetch('https://api.example.com/data', {
4 next: {
5 revalidate: 3600, // Cache for 1 hour
6 tags: ['data'] // For on-demand revalidation
7 }
8 })
9 return res.json()
10}
11
12// Force fresh data
13async function getFreshData() {
14 const res = await fetch('https://api.example.com/data', {
15 cache: 'no-store'
16 })
17 return res.json()
18}
19
20// On-demand revalidation
21import { revalidateTag, revalidatePath } from 'next/cache'
22
23export async function updateData() {
24 await saveToDatabase()
25
26 // Revalidate by tag
27 revalidateTag('data')
28
29 // Or by path
30 revalidatePath('/dashboard')
31}unstable_cache for Database Queries#
1// lib/cache.ts
2import { unstable_cache } from 'next/cache'
3import { prisma } from '@/lib/db'
4
5export const getCachedPosts = unstable_cache(
6 async (userId: string) => {
7 return prisma.post.findMany({
8 where: { authorId: userId },
9 orderBy: { createdAt: 'desc' },
10 take: 10
11 })
12 },
13 ['user-posts'], // Cache key prefix
14 {
15 revalidate: 60, // 1 minute
16 tags: ['posts'] // For manual revalidation
17 }
18)
19
20// With dynamic cache key
21export const getCachedPost = unstable_cache(
22 async (postId: string) => {
23 return prisma.post.findUnique({
24 where: { id: postId },
25 include: { author: true }
26 })
27 },
28 ['post'], // Prefix - actual key will be ['post', postId]
29 { revalidate: 300, tags: ['posts'] }
30)Redis Caching#
1// lib/redis.ts
2import { Redis } from '@upstash/redis'
3
4const redis = new Redis({
5 url: process.env.UPSTASH_REDIS_URL!,
6 token: process.env.UPSTASH_REDIS_TOKEN!
7})
8
9export async function getCached<T>(
10 key: string,
11 fetcher: () => Promise<T>,
12 ttl: number = 3600
13): Promise<T> {
14 // Try cache first
15 const cached = await redis.get<T>(key)
16
17 if (cached !== null) {
18 return cached
19 }
20
21 // Fetch fresh data
22 const data = await fetcher()
23
24 // Cache it
25 await redis.setex(key, ttl, data)
26
27 return data
28}
29
30// Usage
31const user = await getCached(
32 `user:${userId}`,
33 () => prisma.user.findUnique({ where: { id: userId } }),
34 300 // 5 minutes
35)
36
37// Invalidation
38export async function invalidateCache(pattern: string) {
39 const keys = await redis.keys(pattern)
40 if (keys.length > 0) {
41 await redis.del(...keys)
42 }
43}Stale-While-Revalidate Pattern#
1// lib/swr-cache.ts
2import { Redis } from '@upstash/redis'
3
4const redis = new Redis({
5 url: process.env.UPSTASH_REDIS_URL!,
6 token: process.env.UPSTASH_REDIS_TOKEN!
7})
8
9interface CacheEntry<T> {
10 data: T
11 staleAt: number
12 expiresAt: number
13}
14
15export async function swrCache<T>(
16 key: string,
17 fetcher: () => Promise<T>,
18 { staleTime = 60, maxAge = 300 }: { staleTime?: number; maxAge?: number } = {}
19): Promise<T> {
20 const now = Date.now()
21
22 const cached = await redis.get<CacheEntry<T>>(key)
23
24 // Fresh cache hit
25 if (cached && now < cached.staleAt) {
26 return cached.data
27 }
28
29 // Stale cache - return stale data but revalidate in background
30 if (cached && now < cached.expiresAt) {
31 // Revalidate in background (fire and forget)
32 fetchAndCache(key, fetcher, staleTime, maxAge).catch(console.error)
33 return cached.data
34 }
35
36 // Cache miss - fetch fresh
37 return fetchAndCache(key, fetcher, staleTime, maxAge)
38}
39
40async function fetchAndCache<T>(
41 key: string,
42 fetcher: () => Promise<T>,
43 staleTime: number,
44 maxAge: number
45): Promise<T> {
46 const data = await fetcher()
47 const now = Date.now()
48
49 const entry: CacheEntry<T> = {
50 data,
51 staleAt: now + staleTime * 1000,
52 expiresAt: now + maxAge * 1000
53 }
54
55 await redis.setex(key, maxAge, entry)
56 return data
57}Memoization Utilities#
1// lib/memoize.ts
2
3// Simple memoization for synchronous functions
4export function memoize<T extends (...args: any[]) => any>(fn: T): T {
5 const cache = new Map()
6
7 return ((...args: Parameters<T>) => {
8 const key = JSON.stringify(args)
9
10 if (cache.has(key)) {
11 return cache.get(key)
12 }
13
14 const result = fn(...args)
15 cache.set(key, result)
16 return result
17 }) as T
18}
19
20// Async memoization with TTL
21export function memoizeAsync<T extends (...args: any[]) => Promise<any>>(
22 fn: T,
23 ttl: number = 60000
24): T {
25 const cache = new Map<string, {
26 value: Awaited<ReturnType<T>>
27 expires: number
28 }>()
29
30 return (async (...args: Parameters<T>) => {
31 const key = JSON.stringify(args)
32 const now = Date.now()
33
34 const cached = cache.get(key)
35 if (cached && cached.expires > now) {
36 return cached.value
37 }
38
39 const value = await fn(...args)
40 cache.set(key, { value, expires: now + ttl })
41 return value
42 }) as T
43}
44
45// Usage
46const expensiveCalculation = memoize((x: number, y: number) => {
47 // Complex computation
48 return x * y
49})
50
51const fetchUserData = memoizeAsync(
52 async (userId: string) => {
53 return prisma.user.findUnique({ where: { id: userId } })
54 },
55 5 * 60 * 1000 // 5 minutes
56)Cache Warming#
1// lib/cache-warm.ts
2import { getCachedPosts, getCachedPost } from './cache'
3
4export async function warmCache() {
5 // Get popular content
6 const popularPosts = await prisma.post.findMany({
7 where: { viewCount: { gt: 1000 } },
8 take: 100
9 })
10
11 // Pre-cache each post
12 await Promise.all(
13 popularPosts.map(post => getCachedPost(post.id))
14 )
15
16 console.log(`Warmed cache with ${popularPosts.length} posts`)
17}
18
19// Run on app startup or via cron
20// warmCache()Usage Instructions#
- Use React cache for request-level deduplication
- Use Next.js data cache for fetch requests
- Use unstable_cache for database queries with TTL
- Use Redis for distributed caching across instances
- Implement SWR pattern for better UX with stale data
Best Practices#
- Cache strategically - Not everything needs caching
- Set appropriate TTLs - Balance freshness vs performance
- Invalidate correctly - Use tags for related content
- Handle cache failures - Always have a fallback to fetch
- Monitor hit rates - Track cache effectiveness
- Warm critical paths - Pre-cache popular content
- Use cache layers - Combine React, Next.js, and Redis caching
Related Patterns#
- Lazy Loading - Load on demand
- Rate Limiting - Protect endpoints
- Monitoring - Track cache metrics
- ISR - Incremental static regeneration