Skip to content

HTTP Headers Reference: Cache-Control, Security Headers, CORS, Compression & Auth

HTTP headers are the control plane of the web — they govern caching, security, CORS, compression, redirects, and authentication. Getting them right is the difference between a site that’s fast and secure and one that’s leaking information or missing cache hits. This reference covers the headers you’ll actually configure, not the full RFC list.

1. Caching Headers

Cache-Control, ETag, Last-Modified, Vary — what they mean and what values to use
# Cache-Control (the most important caching header):
Cache-Control: no-store               # never cache (sensitive data, user-specific pages)
Cache-Control: no-cache               # cached but must revalidate with server every time
Cache-Control: private, max-age=300   # browser only (not CDN), 5 minutes
Cache-Control: public, max-age=31536000, immutable   # CDN + browser, 1 year (for hashed assets)
Cache-Control: s-maxage=86400         # CDN only (overrides max-age for shared caches)
Cache-Control: stale-while-revalidate=60  # serve stale for 60s while fetching fresh in background

# Pattern: hashed static assets (JS/CSS with content hash in filename):
Cache-Control: public, max-age=31536000, immutable
# immutable = browser won't even revalidate on refresh

# Pattern: HTML pages (never content-hash filenames):
Cache-Control: no-cache
# or:
Cache-Control: public, max-age=0, must-revalidate

# ETag + Last-Modified (conditional requests — saves bandwidth):
# Server sends:
ETag: "abc123"                # hash of response content
Last-Modified: Sat, 14 Mar 2026 18:00:00 GMT

# Browser revalidates with:
If-None-Match: "abc123"       # 304 Not Modified if ETag matches (no body)
If-Modified-Since: Sat, 14 Mar 2026 18:00:00 GMT

# Vary (tell CDN/proxy that response varies by this request header):
Vary: Accept-Encoding          # different response for gzip vs no gzip
Vary: Accept-Language          # different response per language
# Don't use Vary: Cookie or Vary: Authorization — kills CDN caching

2. Security Headers

HSTS, CSP, X-Frame-Options, Permissions-Policy, and Referrer-Policy
# HSTS (HTTP Strict Transport Security — force HTTPS):
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# preload: submit to browser preload list (never HTTP, even first request)

# Content-Security-Policy (prevent XSS — whitelist what can execute):
Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' api.example.com; frame-ancestors 'none'
# Start permissive, tighten with CSP-Report-Only first:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

# X-Frame-Options (prevent clickjacking — use CSP frame-ancestors instead for modern):
X-Frame-Options: DENY                 # cannot be embedded in any iframe
X-Frame-Options: SAMEORIGIN           # only same-origin iframes

# X-Content-Type-Options (prevent MIME sniffing):
X-Content-Type-Options: nosniff       # always include this

# Referrer-Policy:
Referrer-Policy: no-referrer           # never send Referer
Referrer-Policy: strict-origin-when-cross-origin  # full URL same-origin, origin-only cross-origin (recommended default)

# Permissions-Policy (formerly Feature-Policy — restrict browser APIs):
Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
# Empty () = blocked for all. 'self' = allowed for this origin.

# X-Powered-By: remove this — don't advertise your stack:
# Nginx: server_tokens off;
# Express: app.disable('x-powered-by');

3. CORS Headers

Simple vs preflight requests, Access-Control-Allow-*, credentials, and common mistakes
# CORS response headers (server sets these):

# Allow specific origin (don't use * with credentials):
Access-Control-Allow-Origin: https://app.example.com

# For credentials (cookies, Authorization header):
Access-Control-Allow-Origin: https://app.example.com  # must be specific, NOT *
Access-Control-Allow-Credentials: true

# Preflight response (for non-simple methods/headers — OPTIONS request):
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-ID
Access-Control-Max-Age: 86400        # cache preflight for 24h (reduces OPTIONS requests)

# Expose response headers to browser JS (custom headers not visible by default):
Access-Control-Expose-Headers: X-Total-Count, X-Request-ID

# CORS mistakes:
# 1. Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — INVALID
# 2. Forgetting OPTIONS handler in your router — returns 404, CORS fails
# 3. Not including Access-Control-Allow-Headers for Authorization — preflight fails
# 4. Wildcard works for simple GET/POST with no auth — but breaks once you add auth

# Nginx CORS config:
location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '$http_origin';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }
    add_header 'Access-Control-Allow-Origin' '$http_origin' always;
    proxy_pass http://localhost:3000;
}

4. Content Negotiation & Compression

Accept/Content-Type, Accept-Encoding, Content-Encoding, and Link preload hints
# Content negotiation (client tells server what it accepts):
Accept: application/json, text/html;q=0.9, */*;q=0.8
Accept-Language: en-GB,en;q=0.9,fr;q=0.5
Accept-Encoding: gzip, deflate, br, zstd   # Brotli (br) preferred for text
Accept: image/avif, image/webp, image/png, image/svg+xml, */*;q=0.8

# Content-Type (server tells client what it sent):
Content-Type: application/json; charset=utf-8
Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Type: application/octet-stream    # binary file download

# Compression:
Content-Encoding: gzip    # response is gzip-compressed
Content-Encoding: br      # Brotli — 15-20% better than gzip for text

# Nginx compression config:
gzip on; gzip_types text/plain text/css application/json application/javascript;
brotli on; brotli_types text/html text/css application/json application/javascript;

# Link header for resource hints (preload critical assets):
Link: </styles.css>; rel=preload; as=style
Link: </api/user>; rel=preload; as=fetch; crossorigin
Link: <https://fonts.googleapis.com>; rel=preconnect

# Transfer-Encoding: chunked (streaming responses):
Transfer-Encoding: chunked    # server sends data in chunks (SSE, streaming)

5. Auth, Rate Limiting & Request Tracing

Authorization patterns, X-RateLimit-*, correlation IDs, and useful request headers
# Authorization header patterns:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...   # JWT
Authorization: Basic dXNlcjpwYXNz               # base64(user:pass) — HTTPS only
Authorization: ApiKey sk-abc123                  # common API key pattern (non-standard)

# WWW-Authenticate (server challenges unauthenticated request):
WWW-Authenticate: Bearer realm="API", error="invalid_token"
# Response status: 401 Unauthorized

# Rate limiting headers (de facto standard from GitHub/Stripe):
X-RateLimit-Limit: 5000        # max requests per window
X-RateLimit-Remaining: 4998    # remaining in current window
X-RateLimit-Reset: 1710442800  # Unix timestamp when window resets
Retry-After: 60                # seconds to wait (also used for 503)

# Request ID / correlation ID (trace requests across services):
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000   # set by client or gateway
X-Correlation-ID: 550e8400-e29b-41d4-a716-446655440000
X-Trace-ID: 550e8400-e29b-41d4-a716-446655440000
# Log this in every service — makes debugging distributed systems possible

# Forwarded IP (behind proxy/load balancer):
X-Forwarded-For: 203.0.113.1, 10.0.0.1   # original IP, then proxies
X-Real-IP: 203.0.113.1                    # Nginx sets this
Forwarded: for=203.0.113.1;proto=https    # RFC 7239 (modern standard)

# Content-Disposition (file download):
Content-Disposition: attachment; filename="report-2026-03-14.pdf"
Content-Disposition: inline; filename="preview.pdf"   # open in browser

# Useful request debugging:
curl -I https://example.com           # HEAD request, show response headers
curl -v https://example.com           # verbose, show all headers

Track web platform releases at ReleaseRun. Related: NGINX Ingress Reference | OpenSSL Reference | Express.js Reference

🔒 Check Your Headers Live

Use the free HTTP Security Headers Analyzer — paste your response headers, get an instant A-F grade with Nginx snippets for any missing headers.

Founded

2023 in London, UK

Contact

hello@releaserun.com