← Back to articles

How to Implement Caching Strategies (2026)

Caching is the easiest way to make your app faster. Here's every caching layer you should know about and how to implement them.

The Caching Layers

  1. Browser cache — store assets locally
  2. CDN cache — static assets close to users
  3. Application cache — Redis/memory for API responses
  4. Database cache — query results
  5. Computed cache — expensive calculations

Layer 1: Browser Caching (HTTP Headers)

// Next.js API route with cache headers
export async function GET() {
  return new Response(data, {
    headers: {
      'Cache-Control': 'public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800',
    },
  })
}

What this means:

  • public — CDNs can cache it
  • max-age=3600 — browser caches for 1 hour
  • s-maxage=86400 — CDN caches for 24 hours
  • stale-while-revalidate=604800 — serve stale for 7 days while revalidating

Layer 2: CDN Caching (Cloudflare/Vercel)

// Cloudflare Pages Functions
export const onRequest = async (context) => {
  const response = await fetch(apiUrl)
  
  // Cache on Cloudflare's edge
  return new Response(response.body, {
    headers: {
      'Cache-Control': 'public, max-age=3600',
      'CDN-Cache-Control': 'max-age=86400',
    },
  })
}

Next.js static generation:

// app/blog/[slug]/page.tsx
export const revalidate = 3600 // ISR: regenerate every hour

export async function generateStaticParams() {
  const posts = await db.select().from(postsTable)
  return posts.map(post => ({ slug: post.slug }))
}

Layer 3: Redis Application Cache

import Redis from 'ioredis'

const redis = new Redis(process.env.REDIS_URL)

async function getCachedUser(userId: string) {
  // Try cache first
  const cached = await redis.get(`user:${userId}`)
  if (cached) return JSON.parse(cached)

  // Cache miss — fetch from database
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  })

  // Store in cache for 5 minutes
  await redis.setex(`user:${userId}`, 300, JSON.stringify(user))
  
  return user
}

Cache Invalidation

// When user updates profile
await db.update(users).set({ name: 'New Name' }).where(eq(users.id, userId))

// Invalidate cache
await redis.del(`user:${userId}`)

Cache-Aside Pattern (Most Common)

async function getWithCache<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number
): Promise<T> {
  const cached = await redis.get(key)
  if (cached) return JSON.parse(cached)

  const data = await fetcher()
  await redis.setex(key, ttl, JSON.stringify(data))
  return data
}

// Usage
const posts = await getWithCache(
  'posts:recent:10',
  () => db.select().from(posts).limit(10),
  3600
)

Layer 4: Database Query Cache (Postgres)

-- Materialized view for expensive queries
CREATE MATERIALIZED VIEW popular_posts AS
SELECT p.*, COUNT(l.id) as like_count
FROM posts p
LEFT JOIN likes l ON p.id = l.post_id
GROUP BY p.id
ORDER BY like_count DESC
LIMIT 100;

-- Refresh periodically
REFRESH MATERIALIZED VIEW popular_posts;

Layer 5: Computed Results Cache

// Cache expensive computations
async function getRecommendations(userId: string) {
  const cacheKey = `recommendations:${userId}`
  
  const cached = await redis.get(cacheKey)
  if (cached) return JSON.parse(cached)

  // Expensive ML model inference
  const recommendations = await runMLModel(userId)
  
  // Cache for 24 hours
  await redis.setex(cacheKey, 86400, JSON.stringify(recommendations))
  
  return recommendations
}

Caching Strategies

1. Cache-Aside (Lazy Loading)

Most common. Check cache → if miss, fetch and cache.

2. Write-Through

Write to cache and database simultaneously.

async function updateUser(userId: string, data: UpdateData) {
  await db.update(users).set(data).where(eq(users.id, userId))
  await redis.setex(`user:${userId}`, 300, JSON.stringify({ ...data }))
}

3. Write-Behind (Write-Back)

Write to cache immediately, database later (async).

4. Refresh-Ahead

Proactively refresh before expiry.

When to Cache What

Data TypeStrategyTTL
User profilesCache-aside5-15 min
Static contentCDN + long TTL7-30 days
API responsesCache-aside1-60 min
Computed resultsCache-aside1-24 hours
Session dataWrite-throughSession duration

Cache Invalidation

The two hardest problems in CS: naming things and cache invalidation.

// Tag-based invalidation
await redis.set(`post:123`, data, 'EX', 3600, 'TAG', 'posts')
await redis.set(`post:456`, data, 'EX', 3600, 'TAG', 'posts')

// Invalidate all posts
await redis.del(await redis.keys('*TAG:posts*'))

FAQ

Should I cache everything?

No. Cache frequently-read, rarely-changed data. Don't cache user-specific data that changes often.

Redis vs in-memory cache?

Redis for distributed systems (multiple servers). In-memory for single-server apps.

How do I know what to cache?

Monitor slow queries and API response times. Cache the 20% that's requested 80% of the time.

Bottom Line

Start with browser and CDN caching (free, huge impact). Add Redis when you have database bottlenecks. Use Next.js ISR for static generation with periodic revalidation. Cache invalidation is hard — use short TTLs and invalidate on write.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.