UXR SEO Analyzer documentation

Introduction

HTTP Caching Headers

View contents

Introduction

HTTP caching allows browsers to store copies of resources locally, eliminating the need to download them again on subsequent visits. Proper caching can reduce page load times by 50-90% for returning visitors and significantly decrease server load and bandwidth costs.

Understanding how caching headers work is essential for delivering fast, efficient web experiences while ensuring users always see up-to-date content when needed.

How Browser Caching Works

When a browser requests a resource, the server’s response headers tell the browser if and how to cache that resource:

HTTP Caching Flow:
┌─────────────────────────────────────────────────────────────┐
│ First Visit:                                                │
│ 1. Browser requests: GET /styles.css                        │
│ 2. Server responds with resource + cache headers:           │
│    Cache-Control: max-age=31536000                          │
│    ETag: "abc123"                                           │
│ 3. Browser stores resource in local cache                   │
│                                                             │
│ Subsequent Visits (within max-age):                         │
│ 1. Browser checks local cache                               │
│ 2. Cache is fresh → Uses cached copy (no network request!)  │
│ 3. Page loads instantly from cache                          │
│                                                             │
│ After max-age expires:                                      │
│ 1. Browser sends conditional request with ETag              │
│ 2. Server returns 304 Not Modified (if unchanged)           │
│ 3. Browser uses cached copy (minimal bandwidth)             │
└─────────────────────────────────────────────────────────────┘

Key Caching Headers

Cache-Control

The primary header for controlling caching behavior:

Directive Meaning Example Use
max-age=N Cache for N seconds Static assets
no-cache Must revalidate before using HTML pages
no-store Never cache Sensitive data
public Can be cached by CDNs Static resources
private Only browser can cache User-specific data
immutable Never changes (skip revalidation) Versioned assets

ETag (Entity Tag)

A unique identifier for a specific version of a resource:

Server Response:
ETag: "33a64df5"

Browser Revalidation Request:
If-None-Match: "33a64df5"

Server Response (if unchanged):
304 Not Modified

Last-Modified

Timestamp-based validation (older than ETag but still useful):

Server Response:
Last-Modified: Wed, 15 Dec 2025 10:00:00 GMT

Browser Revalidation Request:
If-Modified-Since: Wed, 15 Dec 2025 10:00:00 GMT

Expires (Legacy)

Sets an absolute expiration date. Use Cache-Control: max-age instead when possible.

Caching Strategies by Resource Type

Different resources need different caching strategies:

Resource Type Recommended Cache Policy Why
HTML no-cache or short max-age Content changes frequently
CSS/JS (versioned) max-age=31536000, immutable Filename changes on update
Images max-age=31536000 Rarely change
Fonts max-age=31536000 Never change
API responses no-store or short max-age Data freshness critical

Versioned vs Non-Versioned Assets

Versioned (Recommended):
├── styles.abc123.css  → max-age=1 year (immutable)
├── app.def456.js      → max-age=1 year (immutable)
└── When content changes, filename changes

Non-Versioned (Problematic):
├── styles.css         → max-age=? (risky)
├── app.js             → max-age=? (risky)
└── Users may get stale content

Impact on Performance

Proper caching dramatically improves performance metrics:

Metric First Visit Cached Visit Improvement
Page Load Time 3.2s 0.8s 75% faster
Transferred Data 2.1 MB 45 KB 98% less
Server Requests 45 5 89% fewer
TTFB 450ms 0ms* 100% eliminated

*Cached resources have zero TTFB since no network request is made.

Common Caching Problems

Problem 1: No Cache Headers

Issue: Server doesn’t send caching headers; browser uses heuristics.

Solution: Always explicitly set Cache-Control headers.

Problem 2: Too Short max-age

Issue: Static assets cached for only hours or days.

Solution: Use 1 year (31536000 seconds) for versioned static assets.

Problem 3: Caching HTML

Issue: HTML pages cached too aggressively; users see stale content.

Solution: Use no-cache for HTML to always revalidate.

Problem 4: No ETag/Last-Modified

Issue: Browser can’t revalidate; must re-download expired resources.

Solution: Configure server to send ETags for efficient revalidation.

Checking Your Cache Headers

Using Browser DevTools

  1. Open DevTools → Network tab
  2. Click on any resource
  3. Check Response Headers for:
    • Cache-Control
    • ETag or Last-Modified
    • Expires

Using curl

curl -I https://example.com/styles.css

# Look for:
# Cache-Control: max-age=31536000
# ETag: "abc123"

Using Lighthouse

Run a Lighthouse audit and check:

  • “Serve static assets with an efficient cache policy”
  • Shows resources with short or missing cache TTLs

Implementation

Strategy 1: Cache-Control Directives Deep Dive

Complete Directive Reference

Cache-Control Directives:
┌─────────────────────────────────────────────────────────────┐
│ FRESHNESS DIRECTIVES                                        │
│ ├── max-age=<seconds>      How long cache is fresh          │
│ ├── s-maxage=<seconds>     CDN/proxy cache duration         │
│ └── stale-while-revalidate=<seconds>  Serve stale while     │
│                            fetching fresh copy              │
│                                                             │
│ REVALIDATION DIRECTIVES                                     │
│ ├── no-cache               Always revalidate before using   │
│ ├── must-revalidate        Revalidate after max-age expires │
│ └── proxy-revalidate       Proxies must revalidate          │
│                                                             │
│ STORAGE DIRECTIVES                                          │
│ ├── no-store               Never cache (sensitive data)     │
│ ├── private                Only browser can cache           │
│ └── public                 CDNs and proxies can cache       │
│                                                             │
│ OPTIMIZATION DIRECTIVES                                     │
│ └── immutable              Never revalidate (versioned      │
│                            assets)                          │
└─────────────────────────────────────────────────────────────┘

Combining Directives

# Versioned static assets (CSS, JS with hash in filename)
Cache-Control: public, max-age=31536000, immutable

# HTML pages (always check for updates)
Cache-Control: no-cache

# Sensitive user data (never cache)
Cache-Control: no-store, private

# API responses (short cache with background refresh)
Cache-Control: public, max-age=60, stale-while-revalidate=600

# CDN-specific longer cache
Cache-Control: public, max-age=3600, s-maxage=86400

Strategy 2: Server Configuration

Nginx Configuration

# /etc/nginx/conf.d/caching.conf

# Versioned static assets - cache forever
location ~* \.(css|js)$ {
    # Only if filename contains hash (e.g., app.abc123.js)
    if ($uri ~* ".*\.[a-f0-9]{8,}\.") {
        add_header Cache-Control "public, max-age=31536000, immutable";
    }
    # Non-versioned fallback
    if ($uri !~* ".*\.[a-f0-9]{8,}\.") {
        add_header Cache-Control "public, max-age=86400";
    }
    add_header Vary "Accept-Encoding";
}

# Images and fonts - long cache
location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico|woff|woff2|ttf|eot)$ {
    add_header Cache-Control "public, max-age=31536000";
    add_header Vary "Accept-Encoding";
}

# HTML - always revalidate
location ~* \.html$ {
    add_header Cache-Control "no-cache";
    add_header Vary "Accept-Encoding";
}

# API endpoints - short cache with stale-while-revalidate
location /api/ {
    add_header Cache-Control "public, max-age=60, stale-while-revalidate=600";
    add_header Vary "Accept-Encoding, Authorization";
}

# Enable ETag
etag on;

Apache Configuration

# .htaccess or httpd.conf

<IfModule mod_expires.c>
    ExpiresActive On

    # Default: 1 month
    ExpiresDefault "access plus 1 month"

    # HTML: always revalidate
    ExpiresByType text/html "access plus 0 seconds"

    # CSS/JS: 1 year (use versioned filenames)
    ExpiresByType text/css "access plus 1 year"
    ExpiresByType application/javascript "access plus 1 year"

    # Images: 1 year
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType image/gif "access plus 1 year"
    ExpiresByType image/webp "access plus 1 year"
    ExpiresByType image/svg+xml "access plus 1 year"

    # Fonts: 1 year
    ExpiresByType font/woff "access plus 1 year"
    ExpiresByType font/woff2 "access plus 1 year"
</IfModule>

<IfModule mod_headers.c>
    # Versioned assets (files with hash in name)
    <FilesMatch "\.[a-f0-9]{8,}\.(css|js)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>

    # HTML files
    <FilesMatch "\.html$">
        Header set Cache-Control "no-cache"
    </FilesMatch>

    # Enable ETag
    FileETag MTime Size
</IfModule>

Node.js/Express Configuration

const express = require('express');
const path = require('path');

const app = express();

// Custom caching middleware
const setCacheHeaders = (req, res, next) => {
  const url = req.url;

  // Versioned assets (contain hash)
  if (/\.[a-f0-9]{8,}\.(css|js)$/.test(url)) {
    res.set('Cache-Control', 'public, max-age=31536000, immutable');
  }
  // Images and fonts
  else if (/\.(jpg|jpeg|png|gif|webp|svg|woff2?|ttf|eot)$/.test(url)) {
    res.set('Cache-Control', 'public, max-age=31536000');
  }
  // HTML
  else if (/\.html$/.test(url) || url === '/') {
    res.set('Cache-Control', 'no-cache');
  }
  // API responses
  else if (url.startsWith('/api/')) {
    res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=600');
  }

  next();
};

app.use(setCacheHeaders);

// Serve static files with ETags
app.use(express.static('public', {
  etag: true,
  lastModified: true,
  maxAge: 0, // Let middleware handle Cache-Control
}));

Strategy 3: Asset Versioning (Cache Busting)

Build Tool Configuration

Webpack
// webpack.config.js
module.exports = {
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash:8].css',
    }),
  ],
};
Vite
// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash].[ext]',
      },
    },
  },
});

Why Versioning Matters

Without Versioning:
├── User visits site, downloads styles.css
├── You update styles.css
├── User revisits - browser serves cached (old) styles.css
├── User sees broken layout until cache expires
└── Bad user experience!

With Versioning:
├── User visits site, downloads styles.abc123.css
├── You update CSS, build creates styles.def456.css
├── User revisits - browser requests styles.def456.css (new!)
├── User always sees latest styles
└── Great user experience!

Strategy 4: CDN Caching Configuration

Cloudflare

Page Rules:
URL: example.com/assets/*
Settings:
  - Cache Level: Cache Everything
  - Edge Cache TTL: 1 month
  - Browser Cache TTL: 1 year

URL: example.com/*.html
Settings:
  - Cache Level: Standard
  - Edge Cache TTL: Respect Existing Headers

AWS CloudFront

{
  "CacheBehaviors": [
    {
      "PathPattern": "/assets/*",
      "DefaultTTL": 31536000,
      "MaxTTL": 31536000,
      "MinTTL": 31536000,
      "Compress": true
    },
    {
      "PathPattern": "*.html",
      "DefaultTTL": 0,
      "MaxTTL": 0,
      "MinTTL": 0
    }
  ]
}

Vercel

// vercel.json
{
  "headers": [
    {
      "source": "/assets/(.*)",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    },
    {
      "source": "/(.*).html",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "no-cache"
        }
      ]
    }
  ]
}

Strategy 5: Cache Invalidation

Strategies Comparison

Strategy How It Works Pros Cons
Versioned URLs Change filename Instant, reliable Requires build step
Query strings ?v=123 Easy to implement Some CDNs ignore
CDN purge API call to CDN Flexible Slow, may miss edges
Short TTL Low max-age Simple More requests

Recommended Approach

Best Practice Cache Invalidation:
┌─────────────────────────────────────────────────────────────┐
│ Static Assets (CSS, JS, Images):                            │
│ └── Use content-hash in filename (automatic invalidation)   │
│                                                             │
│ HTML Pages:                                                 │
│ └── Use no-cache (always revalidate)                        │
│                                                             │
│ API Responses:                                              │
│ └── Use stale-while-revalidate for background refresh       │
│                                                             │
│ Emergency Updates:                                          │
│ └── CDN purge API + deploy new versioned assets             │
└─────────────────────────────────────────────────────────────┘

Strategy 6: Vary Header for Conditional Caching

When to Use Vary

# Cache different versions based on encoding
Vary: Accept-Encoding

# Cache different versions for different languages
Vary: Accept-Language

# Cache different for authenticated vs anonymous users
Vary: Authorization

# Multiple conditions
Vary: Accept-Encoding, Accept-Language

Nginx Vary Configuration

# Add Vary header for compressed content
location ~* \.(css|js|html|json|xml)$ {
    add_header Vary "Accept-Encoding";
    gzip on;
}

# Add Vary for language-specific content
location /content/ {
    add_header Vary "Accept-Language";
}

Measuring Cache Effectiveness

Before/After Comparison

BEFORE Cache Optimization:
├── Repeat Visit Page Load: 2.8s
├── Transferred Data: 1.9 MB
├── Server Requests: 42
├── Cache Hit Rate: 15%
└── Lighthouse Performance: 68

AFTER Cache Optimization:
├── Repeat Visit Page Load: 0.6s (↓79%)
├── Transferred Data: 85 KB (↓96%)
├── Server Requests: 8 (↓81%)
├── Cache Hit Rate: 94%
└── Lighthouse Performance: 96 (↑28 points)

Lighthouse Audits

Check for these results:

  • “Serve static assets with an efficient cache policy” - Should pass
  • Shows any resources with cache TTL < 1 week

Testing with DevTools

Network Tab Analysis:
1. Enable "Disable cache" checkbox
2. Load page (simulates first visit)
3. Uncheck "Disable cache"
4. Reload page (simulates repeat visit)
5. Compare "Size" column:
   - "(disk cache)" = cached locally
   - "(memory cache)" = cached in memory
   - Actual size = downloaded from network

Optimization Checklist

Before deploying, verify:

  • [ ] Versioned static assets use max-age=31536000, immutable
  • [ ] HTML uses no-cache for always-fresh content
  • [ ] Images/fonts use max-age=31536000
  • [ ] ETags enabled for efficient revalidation
  • [ ] Vary: Accept-Encoding set for compressed content
  • [ ] CDN caching properly configured
  • [ ] Build tool generates content hashes in filenames
  • [ ] API responses use appropriate caching strategy
  • [ ] Sensitive data uses no-store
  • [ ] Lighthouse caching audit passes

Learn more about optimizing resource delivery:

📚 Back to Performance SEO Hub - Explore all performance topics


References

  1. MDN Web Docs - HTTP Caching
  2. web.dev - HTTP Cache
  3. Chrome Developers - Serve Static Assets with Efficient Cache Policy
  4. Google Developers - HTTP Caching

Try It Yourself

Want to check your site’s caching headers?

🔧 Download UXR SEO Analyzer (Free, 100% local analysis)


Disclaimer: The analyzers in this extension are reference guides based on official documentation from MDN, web.dev, and Chrome Developers. They do not represent absolute truths about how search engines evaluate your content—only search engines know their internal algorithms. Use these recommendations as a starting point to improve your site.

Last updated: December 15, 2025

Related articles

Category hub

Hub

Performance SEO Hub

Performance is a critical ranking factor and directly impacts user experience

In the same category

Detailed guide

Complete TTFB Optimization Guide

Time to First Byte (TTFB) is the foundation of web performance—every millisecond of TTFB delays your entire page load

Introduction

Text Compression

Text compression is one of the most effective ways to reduce file sizes and improve page load times

Introduction

Render-Blocking Resources

Render-blocking resources are files that prevent the browser from displaying content to users until they are fully downloaded and processed

Last updated: