Skip to main content
Web DevelopmentSoftware Engineering

The Developer's Guide to HTTP Caching

Master HTTP caching headers, CDN strategies, and cache invalidation to deliver blazing-fast experiences while keeping content fresh.

Mbeah Essilfie

Mbeah Essilfie

April 24, 2026 at 06:04 AM

9 min read 1826 views
The Developer's Guide to HTTP Caching

Caching Is the Easiest Performance Win

A cached response takes 0ms server time. No optimization beats not doing the work at all.

Cache-Control Header

The single most important caching mechanism:

Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=60

Breaking it down:

  • public — CDNs and browsers can cache this
  • max-age=3600 — Browser cache: 1 hour
  • s-maxage=86400 — CDN cache: 24 hours
  • stale-while-revalidate=60 — Serve stale content while revalidating in background

Caching Strategies by Content Type

// server/middleware/cache-headers.ts
export default defineEventHandler((event) => {
  const path = event.path

  // Static assets: cache forever (use hashed filenames)
  if (path.match(/\.(js|css|woff2|png|jpg|svg)$/)) {
    setHeader(event, 'Cache-Control', 'public, max-age=31536000, immutable')
    return
  }

  // API data: short cache with revalidation
  if (path.startsWith('/api/posts') && event.method === 'GET') {
    setHeader(event, 'Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300')
    return
  }

  // HTML pages: no browser cache, CDN caches briefly
  if (!path.startsWith('/api')) {
    setHeader(event, 'Cache-Control', 'public, s-maxage=300, stale-while-revalidate=60')
    return
  }
})

ETag for Conditional Requests

export default defineEventHandler(async (event) => {
  const post = await getPost(event)
  const etag = `"${hashContent(JSON.stringify(post))}"`

  setHeader(event, 'ETag', etag)

  // If client already has this version, return 304
  const ifNoneMatch = getHeader(event, 'if-none-match')
  if (ifNoneMatch === etag) {
    setResponseStatus(event, 304)
    return null
  }

  return post
})
ETags save bandwidth — a 304 response has no body. The browser reuses its cached version, and the server avoids serializing/transmitting the full response.

Cache Invalidation

The two hard problems in computer science: cache invalidation, naming things, and off-by-one errors.

Strategy 1: Time-based (TTL)

s-maxage=60  ← Content is at most 60 seconds stale

Strategy 2: Event-based purging

// After updating a post, purge CDN cache
async function updatePost(id: string, data: UpdateInput) {
  const post = await prisma.post.update({ where: { id }, data })
  await purgeCDNCache(`/api/posts/${post.slug}`)
  await purgeCDNCache('/api/posts') // List endpoint too
  return post
}

Strategy 3: Versioned URLs

<!-- Hash in filename = cache forever, new file = new URL -->
<script src="/app.a3f2b1c.js"></script>

Common Mistakes

  1. No-cache doesn't mean "don't cache" — It means "revalidate before using cache"
  2. Forgetting Vary header — Different content per Accept-Encoding? Add Vary: Accept-Encoding
  3. Caching authenticated responses — Use private or no-store for user-specific data
  4. No cache-busting for assets — Without hashed filenames, users get stale CSS/JS

Decision Quick-Reference

ContentStrategy
Hashed static assetsimmutable, max-age=1y
HTML pagess-maxage=5m, stale-while-revalidate=1m
Public API (lists)s-maxage=1m, stale-while-revalidate=5m
User-specific dataprivate, no-cache
Real-time datano-store

Caching done right is invisible to users and transformative for performance.

DevOpsPerformanceAPI Design
Mbeah Essilfie

Written by Mbeah Essilfie

Fullstack Software Developer

Read next

The Complete Guide to Vue 3 Composables
12 min read

The Complete Guide to Vue 3 Composables

Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.

Mbeah EssilfieMbeah Essilfie
987