Cache Hit, Miss, and Bypass Mechanics
TL;DR — A cache hit serves a stored response without touching the origin; a miss stores the fetched response for future requests; a bypass fetches from origin and stores nothing. These three states determine your latency budget and origin load at every tier of the HTTP cache hierarchy.
Quick-reference header block for the three canonical scenarios:
# Produces a hit on subsequent requests (shared + browser cache)
Cache-Control: public, s-maxage=86400, max-age=3600
# Produces a miss, then a hit; extends hit window past expiry
Cache-Control: public, s-maxage=86400, stale-while-revalidate=3600
# Produces a bypass — nothing stored at any layer
Cache-Control: no-store, private
Mechanism and RFC 9111 Alignment
RFC 9111 Section 4 defines the evaluation sequence a cache must execute for every incoming request. The outcome — hit, miss, or bypass — follows directly from that sequence.
Cache hit — A stored response satisfies the request without contacting any upstream layer. The cache matches the request URI and method against its index, checks that the stored response is fresh (or has been validated), verifies that Vary headers match the stored entry’s key dimensions, and serves the response. The Age header in the response reflects how long the entry has resided in the shared cache.
Cache miss — No valid stored entry exists. The request routes upstream (to the CDN, then to origin). The response is stored according to the freshness directives it carries, and the Age header resets. Subsequent identical requests will resolve as hits until the TTL expires or the entry is purged. The first request after a cache warm always produces a miss; thundering herd effects during TTL expiry windows occur because many concurrent misses fire simultaneously.
Cache bypass — The request reaches origin but the response is not stored. RFC 9111 mandates bypass when:
no-storeappears in the response — prohibits all storage at every tier.privateappears in a response reaching a shared cache — the entry is only eligible for the user-agent’s private cache.- The HTTP method is not safe and idempotent (
POST,PUT,DELETE,PATCH) — RFC 9111 Section 4 restricts caching toGETandHEADresponses by default. - An
Authorizationrequest header is present and the response does not carrypublic,s-maxage, ormust-revalidate(RFC 9111 Section 3.5).
The distinction between miss and bypass is operationally important: a miss stores the response and improves subsequent requests; a bypass stores nothing, so every future request remains a bypass until the underlying directive changes.
Scope and Precedence
RFC 9111 defines strict directive precedence. The table below shows how the three states are determined across browser (private) and CDN (shared) cache tiers:
| Directive / Condition | Browser cache | Shared cache (CDN) | Resulting state |
|---|---|---|---|
public, max-age=3600 |
Hit after first request | Hit after first request | Hit |
private, max-age=3600 |
Hit after first request | Bypass | Hit (browser only) |
no-cache |
Stored; conditional revalidation required | Stored; conditional revalidation required | Conditional hit (304) or miss |
no-store |
Bypass | Bypass | Always bypass |
s-maxage=86400, max-age=3600 |
Hit for 1 hour | Hit for 24 hours | Split TTL hit |
POST method |
Bypass | Bypass | Always bypass |
Authorization header without public |
Hit (private) | Bypass | Bypass at CDN |
no-store holds absolute precedence over all other directives. It immediately overrides max-age, s-maxage, public, and any extension directive (stale-while-revalidate, immutable). The full precedence resolution order is covered in Header Stacking and Directive Precedence.
s-maxage overrides max-age for shared caches only; browsers ignore s-maxage and apply max-age. This asymmetry enables a long CDN TTL alongside a short browser TTL — a pattern useful for content that must not be stale at the edge for longer than a day but where aggressive browser caching is undesirable.
stale-while-revalidate extends the hit window past max-age expiry by serving the stale entry immediately while asynchronously fetching a fresh copy. During this window the cache state at the CDN is typically reported as STALE or REVALIDATING — not a miss. stale-if-error similarly serves stale content but only on 5xx origin responses; the two are independent and serve different failure modes.
Implementation Patterns
Pattern 1 — Versioned static assets (permanent hits)
Content-hashed filenames guarantee uniqueness, so a one-year TTL is safe. Every request after the first is a hit until the next deployment changes the filename.
Cache-Control: public, max-age=31536000, immutable
immutable tells browsers not to issue conditional revalidation requests during the max-age window, eliminating unnecessary round-trips on page reload.
Pattern 2 — CDN-split TTL for HTML pages
HTML changes frequently enough that a long browser TTL is risky, but CDN edge caching reduces origin load significantly.
Cache-Control: public, s-maxage=3600, max-age=0, must-revalidate
CDN serves hits for up to one hour. The browser receives max-age=0, so it revalidates on every navigation — but the revalidation hits the CDN (which returns 304 cheaply if unchanged), not origin.
Pattern 3 — High-traffic shared API responses with background refresh
Inventory counts, leaderboard scores, or exchange rates benefit from a short CDN TTL with a grace period for background revalidation:
Cache-Control: public, s-maxage=60, stale-while-revalidate=300
Clients within the 5-minute stale window receive the previous entry immediately (a hit) while the CDN refreshes asynchronously. This eliminates thundering-herd misses at the TTL boundary.
Pattern 4 — Authenticated responses
Personalized responses must not be stored by shared caches. The browser may cache privately, subject to the user-agent’s storage limits:
Cache-Control: private, max-age=0, must-revalidate
Vary: Cookie, Authorization
Vary: Cookie prevents CDN response sharing across sessions. See no-cache vs no-store: When to Use Each for the choice between private, max-age=0 and no-store for truly sensitive payloads.
Pattern 5 — Sensitive data (enforce bypass at every tier)
Session tokens, CSRF state, and PCI-scope data must never enter any cache store:
Cache-Control: no-store, private
Every request is a bypass. no-store alone is sufficient; adding no-cache alongside it has no effect because no-store supersedes it.
Server and CDN Configuration
Nginx
# Versioned static assets — permanent hit
location ~* "\.[0-9a-f]{8}\.(js|css|woff2)$" {
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
# HTML pages — CDN caches 1 hour, browser revalidates
location ~* "\.html$" {
add_header Cache-Control "public, s-maxage=3600, max-age=0, must-revalidate" always;
}
# Authenticated API — private cache only
location /api/user/ {
add_header Cache-Control "private, max-age=0, must-revalidate" always;
}
# Session endpoints — absolute bypass
location /auth/ {
add_header Cache-Control "no-store, private" always;
}
The always flag ensures headers are emitted on 4xx and 5xx responses. Without it, Nginx suppresses custom headers on error responses, which can allow error pages to be cached by intermediaries.
Apache
# Versioned static assets
Header always set Cache-Control "public, max-age=31536000, immutable"
# HTML — CDN hit, browser revalidates
Header always set Cache-Control "public, s-maxage=3600, max-age=0, must-revalidate"
# Session endpoints — bypass
Header always set Cache-Control "no-store, private"
Cloudflare (Cache Rules)
In the Cloudflare dashboard under Caching → Cache Rules:
- Static assets rule — URL pattern
*.example.com/assets/*→ Cache eligibility: Eligible for cache → Edge TTL: Ignore Cache-Control and use 1 year. - HTML rule — URL pattern
*.example.com/*.html→ Cache eligibility: Eligible for cache → Edge TTL: Respect Cache-Control → Browser TTL: Respect Cache-Control. - Auth bypass rule — URL pattern
*.example.com/auth/*→ Cache eligibility: Bypass cache. Cloudflare will returnCF-Cache-Status: BYPASSon all matching responses.
Cloudflare maps no-store to a hard BYPASS at its edge automatically. For paths where the origin emits no-store, no Cache Rule is required — Cloudflare respects the header by default.
Interaction with Related Directives
Hit state and stale-while-revalidate
stale-while-revalidate is a hit-state extension. When max-age has elapsed but the stale-while-revalidate window has not, the cache returns the stale entry (a hit) and refreshes in the background. Once the stale-while-revalidate window also expires, the next request becomes a blocking miss. Configure the window to cover expected traffic spikes at TTL boundaries. See How to Calculate Cache Freshness Lifetime for the precise RFC-compliant freshness duration formulas.
Miss state and ETag / Last-Modified validators
A hard miss (no stored entry at all) always results in a full 200 response with payload. A soft miss — where a stored but stale entry exists and the cache issues a conditional request — may result in 304 Not Modified, reusing the stored body. This is the freshness vs validation trade-off: validators save bandwidth on soft misses at the cost of a round-trip. Always include ETag or Last-Modified on cacheable responses to enable 304 savings.
Bypass and Vary
Vary does not cause bypass. It affects cache key partitioning — a response stored under Vary: Accept-Encoding is only a hit for requests with a matching Accept-Encoding value. Requests with a different encoding value produce a miss, not a bypass. An over-broad Vary header (e.g., Vary: User-Agent) fragments the cache into thousands of separate entries, producing near-universal misses — see Mapping Vary Headers to Edge Routing for safe Vary strategies.
Verification Workflow
Step 1 — Inspect cache state headers from the CDN
curl -sI https://example.com/asset.js \
| grep -iE 'cache-control|age|cf-cache-status|x-cache'
Expected output for a hit:
Cache-Control: public, max-age=31536000, immutable
Age: 4327
CF-Cache-Status: HIT
Expected output for a miss (first request):
Cache-Control: public, s-maxage=3600, max-age=0
Age: 0
CF-Cache-Status: MISS
Expected output for a bypass:
Cache-Control: no-store, private
CF-Cache-Status: BYPASS
Step 2 — Force a bypass to test no-store behavior
curl -sI -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://app.example.com/dashboard
This sends a request-side no-cache directive, which instructs any intermediate cache to revalidate rather than serve a stored copy. It does not force bypass if the response is cacheable — use it to test that the origin responds correctly.
Step 3 — Simulate a conditional GET to distinguish soft miss from hard miss
# First: capture the ETag from a fresh response
ETAG=$(curl -sI https://api.example.com/data | grep -i etag | awk '{print $2}' | tr -d '\r')
# Then: send a conditional request
curl -sI -H "If-None-Match: ${ETAG}" https://api.example.com/data
A 304 Not Modified confirms the origin supports conditional validation (soft miss path). A 200 OK with the same ETag indicates the resource has not changed but the origin is ignoring If-None-Match — fix the origin’s validator comparison logic.
Step 4 — Browser DevTools inspection
- Open DevTools → Network tab.
- Load the page without disabling cache.
- Inspect the Size column:
(memory cache)or(disk cache)— browser hit.- A numeric byte count with no cache annotation — network request (miss or bypass).
- Inspect Headers → Response Headers for
Age,CF-Cache-Status, orX-Cacheto determine CDN state.
Step 5 — Verify cache key normalization
# Request with query parameter
curl -sI "https://example.com/api/items?page=1" | grep -i cf-cache-status
# Request without query parameter
curl -sI "https://example.com/api/items" | grep -i cf-cache-status
If the two produce separate MISS responses that never become hits relative to each other, your cache key includes the query string. Normalizing query string parameters in your CDN cache key configuration prevents redundant misses for semantically identical requests.
Failure Modes and Gotchas
-
no-storeon CDN-overridden paths — A CDN Cache Rule with a positive TTL override wins over originno-store. Cloudflare’s “Edge Cache TTL” setting, if applied to a path, overrides theno-storedirective and stores the response anyway. Always verify CDN Cache Rules do not apply positive TTLs to paths that emitno-store. -
Authorizationheader triggering silent bypass — If your API requires anAuthorizationheader and you want CDN caching, you must explicitly addpublicors-maxageto the response. Without it, RFC 9111 Section 3.5 mandates that shared caches bypass the response, producing misses on every CDN request regardless of TTL configuration. -
Thundering herd at TTL boundary — When a popular asset’s
max-ageexpires simultaneously for many CDN PoPs, all issue concurrent misses to origin. Mitigate this withstale-while-revalidateor CDN-level request collapsing — see Origin Shielding and Request Collapsing for how CDN shields absorb this load. -
Vary: *forces bypass — RFC 9111 Section 4.1 specifies thatVary: *makes every response uncacheable. A response carrying this header will always produce a bypass at shared caches. It is typically emitted accidentally by middleware that reflects every request header; audit originVaryheaders before deploying. -
Service worker cache is separate from HTTP cache — A service worker intercepts requests before the HTTP cache and maintains its own
CacheAPI store.no-storeon an HTTP response does not prevent a service worker from storing it. Sensitive data must be explicitly excluded from service worker caching in thefetchevent handler. -
Ageheader rollover on CDN PoP failover — When a CDN fails over to a secondary PoP, the replacement PoP may serve a fresh miss (Age: 0) even though an entry exists globally. This appears as a temporary miss spike during failover events — not a caching bug. -
Pragma: no-cachein responses is non-normative — RFC 9111 definesPragmaonly in request context. Some legacy proxies honour it in responses; do not rely on this behaviour. Always setCache-Controlexplicitly.
FAQ
Q: What is the difference between a cache miss and a cache bypass?
A cache miss means no valid stored entry was found, so the request routes to origin and the response is stored for future requests. A cache bypass means the request intentionally skips storage — the response is fetched from origin but never stored, typically because no-store or a non-cacheable method is in play.
Q: Does the Age header confirm a cache hit?
A non-zero Age header strongly suggests a shared cache served the response. Age: 0 typically means the response was just fetched from origin (a miss or the first request). However, Age: 0 can also appear on a hit if the entry was stored less than one second ago.
Q: Which HTTP methods are never cached?
RFC 9111 only permits caching responses to safe, idempotent methods: GET and HEAD. POST responses may be cached if the response carries explicit freshness directives and a Content-Location header matching the request URI, but this is rare in practice. PUT, DELETE, and PATCH responses are never cached.
Q: Why does an Authorization header cause a bypass by default?
RFC 9111 Section 3.5 prohibits shared caches from storing responses to requests with an Authorization header unless the response explicitly overrides this with public, s-maxage, or must-revalidate. The concern is that one user’s authorized response could be served to a different user.
Q: Can stale-while-revalidate change a miss into a hit?
Yes. During the stale-while-revalidate window the cache serves the expired entry immediately (recorded as a HIT or STALE by most CDNs) while revalidating in the background. The client never experiences a blocking miss, even though the entry’s max-age has elapsed.
Related
- How to Calculate Cache Freshness Lifetime — the RFC 9111 formula for computing a response’s freshness lifetime, including heuristic caching edge cases that affect when hits turn into misses.
- Understanding HTTP Cache Hierarchy — how browser, CDN, and reverse proxy tiers independently apply hit/miss/bypass logic and where their decisions can diverge.
- The Complete HTTP Request Lifecycle — a step-by-step walkthrough of every interception point from DNS to origin response, showing where hit, miss, and bypass decisions are made at each hop.
- Public vs Private Cache Scope — how
publicandprivatedirectives determine whether a response is eligible for a CDN hit or restricted to browser-only storage. - Origin Shielding and Request Collapsing — CDN techniques that collapse concurrent misses into a single origin request, preventing thundering herd effects at TTL boundaries.