Back to Blog
Guide7 min read2026-05-01

HTTP Caching: Browser Cache, CDN, and Cache-Control Headers

Master HTTP caching headers, understand browser vs. CDN caching, and learn how to configure caching for maximum performance.

HTTP Caching: Browser Cache, CDN, and Cache-Control Headers

Caching is one of the highest-ROI performance optimizations available to web developers. A well-cached response is served instantly, without hitting your server at all. This guide covers the HTTP caching model from browser cache to CDN to origin.

How HTTP Caching Works

When a browser or CDN receives a response, it checks the caching headers to determine:

  1. 1Can this response be cached?
  2. 2How long is it fresh?
  3. 3When it expires, can it be revalidated rather than re-fetched?

The primary header controlling this is Cache-Control.

Cache-Control Directives

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

Key directives:

DirectiveMeaning
publicResponse can be cached by browsers and CDNs
privateResponse can only be cached by the browser (not CDN)
no-cacheCache is allowed, but must revalidate before use
no-storeNever cache this response
max-age=NCache for N seconds
s-maxage=NCDN-specific max age (overrides max-age for CDNs)
stale-while-revalidate=NServe stale while revalidating in background
immutableContent will never change; skip revalidation

Caching Static Assets

Fingerprinted assets (e.g., main.a3f7b1.js) can be cached forever — the filename changes on each build.

location ~* .(js|css|png|jpg|jpeg|gif|ico|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

With fingerprinting: when the file changes, the URL changes, so cached versions are never stale.

Caching HTML

HTML should not be cached aggressively — it references your versioned assets:

location ~* .html$ {
    add_header Cache-Control "no-cache, must-revalidate";
}

no-cache doesn't mean "don't cache" — it means "cache, but always revalidate." The browser sends a conditional request, and if the content hasn't changed, the server returns 304 Not Modified with no body — fast and bandwidth-efficient.

ETag and Last-Modified (Conditional Requests)

These headers enable revalidation:

  • ETag: A hash of the content. Browser sends If-None-Match on revalidation.
  • Last-Modified: Timestamp. Browser sends If-Modified-Since on revalidation.
# First request
GET /index.html
← 200 OK
← ETag: "abc123"
← Last-Modified: Mon, 01 Jan 2025 00:00:00 GMT

# Revalidation request
GET /index.html
→ If-None-Match: "abc123"
← 304 Not Modified (no body, uses cached version)

Node.js/Express sends ETags automatically. Nginx does too for static files.

CDN Caching

A CDN (Cloudflare, AWS CloudFront, Fastly) caches responses at edge nodes worldwide. The flow:

User → CDN Edge (cache hit) → Response
User → CDN Edge (cache miss) → Origin Server → CDN Edge (stores) → Response

Use s-maxage to give CDNs a different TTL than browsers:

Cache-Control: public, max-age=60, s-maxage=3600

Browser caches for 1 minute; CDN caches for 1 hour. Good for semi-dynamic content — users get fresh data, but your origin isn't hammered.

Vary Header

The Vary header tells caches to store separate versions of a response based on request headers:

Vary: Accept-Encoding

This tells caches to store separate copies for gzip and brotli clients. If you serve different content based on Accept-Language, add it to Vary.

Cache Busting Strategies

When you need to invalidate cached content:

  1. 1Fingerprinting — Change the filename on every build (webpack/Vite do this automatically).
  2. 2Query parameters/app.js?v=2 (less reliable, some CDNs ignore query strings).
  3. 3CDN purge API — Cloudflare, CloudFront, etc. offer API endpoints to purge specific URLs.
  4. 4Versioned paths/v2/app.js (robust but requires coordinated deploys).

Practical Cache Strategy

Content TypeCache-ControlNotes
Fingerprinted JS/CSSpublic, max-age=31536000, immutableForever (URL changes on build)
Images (fingerprinted)public, max-age=31536000, immutableSame
HTML pagesno-cacheAlways revalidate
API responses (public)public, max-age=60, s-maxage=300Short cache, CDN layer
API responses (private)private, max-age=30Browser only
Authenticated responsesno-storeNever cache

Express.js Caching Headers

// Cache static assets aggressively
app.use('/static', express.static('public', {
  maxAge: '1y',
  immutable: true
}));

// API route with short cache
app.get('/api/public-data', (req, res) => {
  res.set('Cache-Control', 'public, max-age=60, s-maxage=300');
  res.json(data);
});

// No caching for authenticated routes
app.get('/api/user', authenticate, (req, res) => {
  res.set('Cache-Control', 'no-store');
  res.json(user);
});

Conclusion

HTTP caching is a layered system. Browser caches reduce repeat requests. CDNs reduce origin load. ETags enable efficient revalidation. Use aggressive caching for fingerprinted assets, conservative caching for HTML, and never cache sensitive authenticated responses. Deploy your optimally-cached app on PandaStack at [dashboard.pandastack.io](https://dashboard.pandastack.io) — see [docs.pandastack.io](https://docs.pandastack.io) for deployment options.

Ready to deploy?

Start free on PandaStack — no credit card required.

Start free on PandaStack

More in Guide

Browse all Guide articles →

See also