Skip to content
← All articles

HTTP Cache-Control Headers: Complete Caching Guide

The Cache-Control header is the main tool for HTTP caching control. A correct setup reduces server load, accelerates the site for users, and improves PageSpeed анализ. A wrong setup leads to "stuck" content, missing updates, and SEO problems.

This guide covers every Cache-Control directive, relationships with ETag, Last-Modified, and Vary, strategies for different content types (static, HTML, API документацию), and real nginx and Apache examples.

What Cache-Control Is

Per RFC 9111, Cache-Control governs the behavior of caches (browser, CDN, proxy). It is the standard since 1999 (HTTP/1.1), replacing the legacy Expires and Pragma headers.

Key Directives

max-age=N

Cache lifetime in seconds. max-age=3600 = cache valid for 1 hour.

s-maxage=N

Like max-age, but only for shared cache (CDN, reverse proxy), ignored by browsers.

public

Allowed for both shared (CDN) and private (browser) caches.

private

Browser only. Forbids CDN caching (for authenticated content).

no-cache

Does NOT mean "do not cache"! It means "always revalidate with origin via ETag/If-Modified-Since before using cache." If 304 is returned — use cache.

no-store

This one truly means "do not cache at all." No browser, CDN, or proxy should store the response.

must-revalidate

Forbids using stale cache without revalidation. Useful for financial data.

immutable

A promise: content will never change. Browser does not revalidate even on reload.

stale-while-revalidate=N

Allows serving stale cache for N seconds while revalidation happens in background. Instant response + fresh content on the next request.

stale-if-error=N

If origin fails, serve stale cache for N seconds. Downtime protection.

Common Patterns

Hashed static assetsCache-Control: public, max-age=31536000, immutable # 1 year, immutable — webpack bundle app.a3f2b.js will not change

HTML pages

Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400
# Browser: always revalidates
# CDN: caches 1 hour
# After expiry — stale-while-revalidate for a day

API endpoints (dynamic)

Cache-Control: private, max-age=60
# Private cache 60 sec, CDN does not cache

Authenticated content

Cache-Control: private, no-cache
Vary: Authorization, Cookie

Sensitive data (banking, passwords)

Cache-Control: no-store
Pragma: no-cache

nginx Configuration

server {
    # Hashed static — long immutable cache
    location ~* \.(css|js|woff2|png|jpg|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Unhashed images — moderate cache
    location ~* \.(png|jpg|gif)$ {
        expires 7d;
        add_header Cache-Control "public, max-age=604800";
    }

    # HTML — minimal cache, stale-while-revalidate
    location ~* \.html$ {
        expires 0;
        add_header Cache-Control "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400";
    }

    # API — private, short
    location /api/ {
        add_header Cache-Control "private, max-age=60";
        proxy_pass http://backend;
    }

    # Sensitive
    location /api/account {
        add_header Cache-Control "no-store";
    }
}

Apache Configuration (.htaccess)

<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/jpeg "access plus 7 days"
    ExpiresByType text/css "access plus 1 year"
    ExpiresByType application/javascript "access plus 1 year"
</IfModule>

<IfModule mod_headers.c>
    <FilesMatch "\.(css|js|woff2)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>

    <FilesMatch "\.html$">
        Header set Cache-Control "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400"
    </FilesMatch>
</IfModule>

ETag and Last-Modified: Revalidation

When cache expires, the browser issues a conditional request:

GET /page.html
If-None-Match: "a3f2b"
If-Modified-Since: Wed, 01 Jan 2026 12:00:00 GMT

Server responds with 304 Not Modified (empty body) or 200 with new content. 304 is cheaper than a full transfer.

# nginx auto-emits ETag for static
etag on;

# PHP
$etag = md5_file($path);
header("ETag: \"{$etag}\"");
if (($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') === "\"{$etag}\"") {
    http_response_code(304);
    exit;
}

Vary — Accounting for Representations

Vary: Accept-Encoding
# Separate caches per gzip/br/identity

Vary: Accept-Language
# Separate caches per ru/en versions

Vary: Cookie
# Breaks shared cache — use only when necessary

Cache-Busting Strategies

  1. Fingerprinting (hash in filename)app.a3f2b.js. Most reliable.
  2. Query stringapp.js?v=2. Works, but some CDNs ignore query.
  3. Versioning path/v2/app.js. Full invalidation on release.
  4. ETag-based — only for HTML, not for static assets.

CDN Specifics

Verifying Caching

Use the HTTP Header Checker — it displays Cache-Control, ETag, Last-Modified, Age, and CF-Cache-Status in one place. Our full caching guide covers advanced examples.

# Terminal check
curl -I https://example.com/app.js

# With revalidation
curl -H 'If-None-Match: "a3f2b"' https://example.com/app.js
# expect HTTP/2 304

Anti-patterns

Frequently Asked Questions

Q: Why does no-cache still cache?
A: no-cache ≠ no-store. no-cache allows storing but requires revalidation. For full disable — no-store.

Q: max-age or Expires — which is better?
A: max-age. Expires is a legacy HTTP/1.0 header that relies on correct server time. Cache-Control takes precedence.

Q: Cache stuck after site update?
A: Use fingerprinting — a new hash in the filename gives a new URL, and the browser loads the fresh version.

Q: Does caching affect SEO?
A: Indirectly via Core Web Vitals. Correct caching reduces TTFB and improves LCP.

Conclusion

Cache-Control is powerful but requires understanding of directives. Hashed static = immutable for a year. HTML = short max-age + stale-while-revalidate. Authenticated = private or no-store. Always verify real headers via the Enterno.io HTTP Checker after changes.

Check your website right now

Check now →
More articles: HTTP
HTTP
HTTP 503 Service Unavailable: Causes and Solutions
15.04.2026 · 5 views
HTTP
HTTP Methods Explained: GET, POST, PUT, DELETE and Beyond
16.03.2026 · 89 views
HTTP
The Complete HTTP Request Lifecycle: From URL to Rendered Page
16.03.2026 · 55 views
HTTP
HTTP Headers: The Complete Guide
10.03.2025 · 51 views