Header: Cache-Control
Purpose: Controls caching behavior to prevent sensitive data leakage
Severity if Missing: Medium (for sensitive pages)
Complexity: Medium
Best Practice (Sensitive Pages):
Cache-Control: no-store, no-cache, must-revalidate, private
Cache-Control directives control how browsers and intermediate proxies cache responses. For pages containing sensitive information (login forms, account details, personal data), proper cache settings prevent data from being stored and potentially accessed by unauthorized parties.
Prevent Caching:
no-store- Never cache (strongest for sensitive data)no-cache- Cache but revalidate before usemust-revalidate- Force revalidation when stale (prevents serving stale content if origin unreachable)
Scope:
private- Only browser cache, not shared proxiespublic- Can be cached by any cache (avoid for sensitive content)
Timing:
max-age=<seconds>- How long to caches-maxage=<seconds>- Shared cache (proxy) max age (overrides max-age for shared caches)
Modern Stale Directives (RFC 5861):
stale-while-revalidate=<seconds>- Serve stale content while revalidating in backgroundstale-if-error=<seconds>- Serve stale content if origin is down/errorsimmutable- Content will never change (strong hint for long-lived resources)
Without proper Cache-Control:
User logs into banking site on library computer
Response headers:
Cache-Control: public, max-age=3600Browser caches account page for 1 hour
User logs out and leaves
Next user on same computer:
- Presses back button
- Browser shows cached account page
- Sees previous user's account balance, transactions
Result: Privacy breach
With proper Cache-Control:
Cache-Control: no-store, privateBrowser never caches page → Back button shows login page
Scenario: Conflicting public + private directives
Response headers:
Cache-Control: public, private, max-age=3600Problem: Mutually exclusive directives create undefined behavior.
Browser interpretation varies:
- Some browsers treat
publicas last-wins - Others treat
privateas more restrictive (last-wins) - Inconsistent behavior across browsers = unpredictable security
Result: Some users' sensitive data may be cached in shared proxies despite private directive.
Secure Configuration:
Remove conflicting directive:
Cache-Control: private, max-age=3600Browser behavior is now consistent → Sensitive data stays in browser cache only.
Scenario: no-store with redundant max-age
Response headers:
Cache-Control: no-store, max-age=3600Problem: Redundant max-age masks security intent and creates confusion.
Developer confusion:
- "Why is max-age here if we're not storing anything?"
- Future developer removes
no-storethinking max-age is the control - Creates maintenance risk
Clear Configuration:
Cache-Control: no-store, privateScenario: Long-lived cache without must-revalidate
Response headers:
Cache-Control: max-age=604800 (7 days)User accesses site → content cached for 7 days
Day 5: Origin server goes down
Day 6: User returns to site
- Browser has stale content (6 days old, 1 day before expiry)
- Origin unreachable → cannot revalidate
- Browser serves stale content despite origin being down
Result: User sees outdated/incorrect information for up to 1 more day.
With must-revalidate:
Cache-Control: max-age=604800, must-revalidateDay 6: User returns to site
- Browser has stale content (6 days old, 1 day before expiry)
- Origin unreachable → must-revalidate prevents serving stale
- Browser shows error page
Result: User sees error instead of potentially dangerous stale data.
The analyzer detects these common directive conflicts:
Severity: MEDIUM
Example:
Cache-Control: public, private
Problem: These directives are mutually exclusive. Browsers may interpret differently.
Fix: Remove one directive.
Cache-Control: private (for sensitive data)
Cache-Control: public (for static resources)
Severity: LOW
Example:
Cache-Control: no-store, max-age=3600
Problem: max-age is redundant when no-store is present. Creates confusion.
Fix: Remove max-age directive.
Cache-Control: no-store, private
Severity: LOW
Example:
Cache-Control: no-store, no-cache
Problem: no-cache is redundant when no-store is present (no-store is stronger).
Fix: Remove no-cache directive.
Cache-Control: no-store, private
Severity: LOW
Example:
Cache-Control: private, s-maxage=3600
Problem: s-maxage only applies to shared caches. private prevents shared caching, making s-maxage inapplicable.
Fix: Remove s-maxage directive.
Cache-Control: private, max-age=3600
Use must-revalidate when:
- Long cache durations (>1 day) - Prevents serving very stale content
- Critical content accuracy - Financial data, stock prices, medical info
- Regulatory compliance - When serving outdated data violates rules
Example:
Cache-Control: max-age=86400, must-revalidate (1 day)
Don't use when:
-
Using immutable - Already guarantees content never changes
Cache-Control: public, max-age=31536000, immutable -
Very short max-age (<1 hour) - Overhead not worth it
Cache-Control: max-age=300 (5 minutes - no must-revalidate needed) -
Using no-store - Already prevents caching entirely
Cache-Control: no-store, private
Allows serving slightly stale content while fetching fresh content in background.
Syntax:
Cache-Control: max-age=600, stale-while-revalidate=30
How it works:
- Content fresh for 10 minutes (max-age=600)
- Content stale after 10 minutes
- For next 30 seconds, browser serves stale content immediately
- Browser revalidates in background
- Next request gets fresh content
Use case: Improve perceived performance for frequently changing content.
Allows serving stale content if origin errors (5xx) or is unreachable.
Syntax:
Cache-Control: max-age=600, stale-if-error=86400
How it works:
- Content fresh for 10 minutes
- If origin returns error or is down, serve stale content
- Can serve stale for up to 24 hours (86400 seconds)
Use case: Graceful degradation when origin is down.
Optimal for resilience + performance:
Cache-Control: max-age=600, stale-while-revalidate=30, stale-if-error=86400
- Fresh for 10 minutes
- Stale-while-revalidate for 30 seconds (fast response)
- Stale-if-error for 24 hours (resilience to outages)
Cache-Control: no-store, private
Clean, no conflicts. Use for: Login pages, account pages, checkout, personal data.
Cache-Control: no-store, no-cache, must-revalidate, private
Works but has redundant directives (no-cache redundant with no-store). First example is cleaner.
Cache-Control: public, max-age=31536000, immutable
Use for: Images, CSS, JS with versioned URLs (style.v123.css).
Cache-Control: public, max-age=604800, must-revalidate
7-day cache with revalidation safeguard. Use for: unversioned static content.
Cache-Control: max-age=600, stale-while-revalidate=30, stale-if-error=3600
Modern caching with graceful degradation. Use for: API responses, news feeds.
Cache-Control: private, max-age=0
Caches briefly, requires revalidation.
Cache-Control: max-age=604800
Long cache (7 days) without must-revalidate. Analyzer recommends adding must-revalidate.
Cache-Control: public, max-age=86400
Sensitive data cached for 24 hours in shared caches.
Cache-Control: public, private
Mutually exclusive directives. Analyzer detects conflict (MEDIUM severity).
Cache-Control: no-store, max-age=3600
Redundant max-age with no-store. Analyzer flags as conflict (LOW severity).
1. Caching Login Pages
Cache-Control: public # Wrong for login!
Fix: Use no-store for authentication pages.
2. Confusing no-cache and no-store
no-cachestill caches (just revalidates)no-storenever caches Useno-storefor sensitive data.
3. Not Setting private for User Data
User-specific content should be private to avoid proxy caching.
4. Directive Conflicts (NEW)
Cache-Control: public, private, max-age=3600
Fix: Remove conflicting directives. Use only private for user data.
5. Redundant Directives (NEW)
Cache-Control: no-store, max-age=3600
Fix: Remove max-age when using no-store. They conflict.
6. Long Cache Without Revalidation Safeguard (NEW)
Cache-Control: max-age=604800 # 7 days, no must-revalidate
Fix: Add must-revalidate for long cache durations (>1 day):
Cache-Control: max-age=604800, must-revalidate
7. Using s-maxage with private (NEW)
Cache-Control: private, s-maxage=3600
Fix: Remove s-maxage - it's inapplicable when using private (no shared caching).
Express.js:
// Sensitive pages
res.setHeader('Cache-Control', 'no-store, private');
// Static assets
res.setHeader('Cache-Control', 'public, max-age=31536000');Nginx (sensitive):
add_header Cache-Control "no-store, no-cache, must-revalidate" always;Nginx (static):
location ~* \.(jpg|jpeg|png|css|js)$ {
add_header Cache-Control "public, max-age=31536000";
}All browsers support Cache-Control.
- Best Practices
- Analyzer Reference
- RFC 7234: HTTP Caching
- RFC 5861: HTTP Cache-Control Extensions for Stale Content
Last Updated: 2026-01-01 Status: Active