[Cache] Document CDN-Cache-Control as a BYPASS cause - #32569
Conversation
Review
👉 Fix in your agent 👈Fix the following review findings in PR #32569 (https://github.com/cloudflare/cloudflare-docs/pull/32569).
Before making changes, review each finding and present a brief summary table:
- For each finding, state whether you agree, disagree, or need clarification
- If you disagree (e.g. the fix requires disproportionate effort for minimal benefit,
or the finding is factually incorrect), explain why
- If you need clarification before deciding, ask those questions
- Then share your plan for which issues to tackle and in what order
After triaging, follow this order:
1. Post a comment on this PR for any findings you are skipping, with the finding ID and your reasoning.
2. Then commit the fixes for the legitimate findings.
The comment must come before the commit — the bot reads PR comments when a new
push triggers a review, so skip comments posted after the push will be missed.
---
## Code Review
### Warnings (2)
#### CR-6e16d4b86f8b · Inconsistent CDN-Cache-Control BYPASS guidance
- **File:** `src/content/docs/cache/troubleshooting/investigating-uncached-responses.mdx` line 44
- **Issue:** Line 44 claims `no-cache` and `max-age=0` in `Cloudflare-CDN-Cache-Control` or `CDN-Cache-Control` do not produce `BYPASS`. However, `src/content/docs/cache/concepts/cache-control.mdx` states that when `Cloudflare-Cdn-Cache-Control` is set, only `s-maxage`, `must-revalidate`, or `public` directives allow caching and 'if any other directive is present, we do not cache the asset and instead return `BYPASS`.' This contradicts the claim that `no-cache`/`max-age=0` in these headers do not produce `BYPASS`.
- **Fix:** Confirm the correct behavior and update either this page or the Origin Cache Control concept page so the two documents give consistent guidance.
#### CR-16749eabeb7e · Contradicts existing Authorization behavior
- **File:** `src/content/docs/cache/concepts/cache-responses.mdx` line 58
- **Issue:** The new bullet states that `no-cache`, `max-age=0`, or `s-maxage=0` in `Cloudflare-CDN-Cache-Control` or `CDN-Cache-Control` "do not produce `BYPASS`" and instead produce `MISS` then `REVALIDATED`/`EXPIRED`. However, `/cache/concepts/cache-control/#conditions` states that when `Cloudflare-Cdn-Cache-Control` is set, OCC is forced on and the Authorization logic permits caching only if the header contains `s-maxage`, `must-revalidate`, or `public`; any other directive returns `BYPASS`. Because `no-cache` and `max-age=0` are not in that list, a request with an `Authorization` header and a `Cloudflare-CDN-Cache-Control: no-cache` (or `max-age=0`) response would be `BYPASS`, not cache-and-revalidate. The blanket statement conflicts with the existing Authorization caveat documented on the same page for `Cache-Control`.
- **Fix:** Add the same `Authorization` caveat used for `Cache-Control` — or explicitly scope the "does not produce BYPASS" claim to the non-Authorization case — and reconcile the wording with the existing note in `/cache/concepts/cache-control/`.
---
## Style Guide Review
### Warnings (1)
#### SG-a13f75382190 · HTTP headers must use monospace
- **File:** `src/content/docs/cache/troubleshooting/investigating-uncached-responses.mdx` line 43
- **Issue:** Line contains `ignores origin cache-control` where the Cache-Control header reference is not in backticks.
- **Fix:** Change `origin cache-control` to `origin Cache-Control` or `origin \`Cache-Control\``.
Code ReviewThis code review is in beta and may not always be helpful — use your judgment. Warnings (2)
ConventionsNo convention issues found. Style Guide ReviewWarnings (1)
CommandsOnly codeowners can run commands. Post a comment with the command to trigger it.
|
|
This pull request requires reviews from CODEOWNERS as it changes files that match the following patterns:
|
| Common reasons the origin response is treated as not cacheable include: | ||
|
|
||
| * The response exceeds the [maximum cacheable file size](/cache/concepts/default-cache-behavior/#cacheable-size-limits) for your plan. | ||
| * The origin returned a `Cloudflare-CDN-Cache-Control` or `CDN-Cache-Control` header with a non-cacheable directive. Cloudflare evaluates these headers ahead of `Cache-Control`, in the precedence `Cloudflare-CDN-Cache-Control` > `CDN-Cache-Control` > `Cache-Control`. For example, an origin returning both `Cache-Control: public, max-age=3600` and `CDN-Cache-Control: no-store` produces `BYPASS`. Inspect every response header, not just `Cache-Control`. Refer to [CDN-Cache-Control](/cache/concepts/cdn-cache-control/) for the precedence rules. |
There was a problem hiding this comment.
good call, maybe we want to mention Surrogate-Control too.
Cloudflare-CDN-CC > CDN-CC > Surrogate-Control > Cache-Control
cc @zaidoon1
|
Both changes LGTM. Left some nits to make the existing change more clear, thanks for the effort @ngayerie |
7ebf475 to
2f583e2
Compare
|
Hi @mbullock1986 @zaidoon1 |
- Add a BYPASS bullet documenting Cloudflare-CDN-Cache-Control and CDN-Cache-Control, and their precedence over Cache-Control. An origin returning cacheable Cache-Control alongside a non-cacheable CDN-Cache-Control directive produces BYPASS, and readers otherwise have no signal that these two headers even exist as BYPASS causes. - Rewrite the Age callout and HIT confirmation to describe the Tiered Cache lower-tier fill in plain language, without referring to the internal CacheTieredFill=true field. That field is only visible in the http_requests Logpush dataset, not in HTTP responses, so it is not something readers can inspect.
Address SG-619a15c3d17a from the docs bot review.
Clarify that Age is per-data-center scope: the seconds since the serving data center's local cache admitted or last revalidated the object, rather than the vaguer 'time in Cloudflare's cache'. Applies @foreseaz's suggestion.
2f583e2 to
ee9f456
Compare
zaidoon1
left a comment
There was a problem hiding this comment.
Checked the CDN-Cache-Control and Age claims against actual behavior. The BYPASS bullet's core point is right, but a few details need tightening. Comments inline.
| Common reasons the origin response is treated as not cacheable include: | ||
|
|
||
| * The response exceeds the [maximum cacheable file size](/cache/concepts/default-cache-behavior/#cacheable-size-limits) for your plan. | ||
| * The origin returned a `Cloudflare-CDN-Cache-Control` or `CDN-Cache-Control` header with a non-cacheable directive. Cloudflare evaluates these headers ahead of `Cache-Control`, in the precedence `Cloudflare-CDN-Cache-Control` > `CDN-Cache-Control` > `Cache-Control`. For example, an origin returning both `Cache-Control: public, max-age=3600` and `CDN-Cache-Control: no-store` produces `BYPASS`. Inspect every response header, not just `Cache-Control`. Refer to [CDN-Cache-Control](/cache/concepts/cdn-cache-control/) for the precedence rules. |
There was a problem hiding this comment.
The example reads as unconditional. An Edge TTL that ignores origin cache-control overrides CDN-Cache-Control: no-store the same way it overrides Cache-Control: no-store, so with that rule in place these two headers give MISS then HIT. The no-store bullet right below already carries that exception, and the CDN-Cache-Control page says Edge Cache TTL wins.
| The `Age` response header is returned when Cloudflare serves a response from a local cache object on the data center handling the request. It is the number of seconds since that object was admitted or last revalidated at that cache. It resets if the asset is revalidated, purged, or evicted and then re-cached. | ||
|
|
||
| The `Age` header is only present for responses served from the cache. It will not appear on a cache MISS, dynamic traffic, the first request that populates the lower tier HIT from tiered cache `CacheTieredFill=true` or any responses that did not originate from the cache (for example, responses generated by a Worker that bypassed the cache). | ||
| The `Age` header is only present for responses served from the cache. It will not appear on a cache MISS, dynamic traffic, the first request that populates a lower-tier data center's cache from an upper tier via [Tiered Cache](/cache/how-to/tiered-cache/), or any responses that did not originate from the cache (for example, responses generated by a Worker that bypassed the cache). |
There was a problem hiding this comment.
Too strong in both directions.
Age is absent on EXPIRED and REVALIDATED when the request actually revalidated against the origin, and those still count as served from cache. It's present on HIT, STALE, and UPDATING.
Other direction: an Age set by the origin still reaches the client on uncacheable responses. Origin sends Age: 42, status comes back DYNAMIC or BYPASS, client sees Age: 42. So "will not appear on a cache MISS" only holds when the origin didn't send one.
| The request was eligible for cache, but the origin response or configuration prevented Cloudflare from storing it. Common causes: | ||
|
|
||
| - **The response exceeds the [cacheable size limit](/cache/concepts/default-cache-behavior/#cacheable-size-limits) for your plan.** Split the object into smaller assets, or move to a plan with a higher limit. [R2](/r2/) is an origin storage alternative — it does not raise the CDN cacheable size limit. | ||
| - **The origin returned a `Cloudflare-CDN-Cache-Control` or `CDN-Cache-Control` header with a non-cacheable directive.** Cloudflare evaluates these headers ahead of `Cache-Control`, in the precedence `Cloudflare-CDN-Cache-Control` > `CDN-Cache-Control` > `Cache-Control`. For example, an origin returning both `Cache-Control: public, max-age=3600` and `CDN-Cache-Control: no-store` produces `BYPASS`. Inspect every response header, not just `Cache-Control`. Refer to [CDN-Cache-Control](/cache/concepts/cdn-cache-control/) for the precedence rules. |
There was a problem hiding this comment.
"Inspect every response header" won't surface Cloudflare-CDN-Cache-Control. Cloudflare doesn't return that one to the client, so it's invisible from the response side. That's the useful bit for a troubleshooting page: if you see BYPASS with no CDN-Cache-Control in the response, check what the origin actually sent, or have the origin drop the header and retest.
Same two points I left on the cache-responses.mdx bullet apply here.
cache-responses.mdx:
- Age callout scope: drop 'at that cache' — Age tracks the object's age
in Cloudflare's network-wide cache, not a single data center. A HIT
served locally can carry an Age inherited from an upper tier under
Tiered Cache.
- Age status enumeration: rewrite by cf-cache-status. Age is set on
HIT, STALE, UPDATING. It is absent on MISS, on EXPIRED and REVALIDATED
when the request revalidated against the origin, and on DYNAMIC,
BYPASS, NONE/UNKNOWN. Origin-set Age on uncacheable responses is
proxied to the client — a DYNAMIC or BYPASS response can carry an
Age value that came from the origin, not from Cloudflare.
- CDN-Cache-Control BYPASS bullet:
- Name the directives: only no-store and bare private produce
BYPASS. no-cache, max-age=0, s-maxage=0 do not — they produce
MISS then REVALIDATED or EXPIRED.
- Add the Edge Cache TTL override, same as for Cache-Control:
no-store.
- Drop the Origin Cache Control condition — these headers always
follow OCC-style semantics regardless of the zone setting.
investigating-uncached-responses.mdx:
- Mirror the same CDN-Cache-Control corrections, framed for the
troubleshooting reader: note that Cloudflare does not forward
Cloudflare-CDN-Cache-Control to the client, so a BYPASS without a
visible CDN-Cache-Control header means checking what the origin
actually sent (or dropping the header at origin and retesting).
- HIT verification Age bullet: reconcile with the Age scope correction.
With Tiered Cache, a fresh local fill can already report a large Age
inherited from an upper tier — not 'may be absent'.
|
[AI] Thanks @zaidoon1 — all 5 comments applied in
Also reconciled the HIT verification bullet on the same page with your Age scope correction — a fresh local fill from an upper tier can already report a large |
zaidoon1
left a comment
There was a problem hiding this comment.
Latest addresses everything I raised. One clause got lost in the rewrite that I'd put back, plus a couple of small things.
cache-responses.mdx: - Age callout: qualify the reset rule as 'revalidation against the origin'. With Tiered Cache, a lower tier revalidating against an upper tier that still has the object picks up the upper tier's age rather than resetting to zero. - CDN-Cache-Control BYPASS bullet: split the sentence on the no-cache/max-age=0/s-maxage=0 clarification per style guide (no semicolons). investigating-uncached-responses.mdx: - CDN-Cache-Control BYPASS bullet: pull the no-cache/max-age=0/ s-maxage=0 clarification into its own bullet. Six-sentence walls are hard to scan while debugging, and the clarification points the reader elsewhere (to Repeated MISS), so it earns a separate bullet. Also removes the semicolon flagged on the reference page. - HIT verify Age bullet: restore the absent-on-fill case. On the request that fills a lower tier from an upper tier, the client gets HIT with no Age at all — the HIT reflects the upper tier, and the local data center has not served it from cache yet. The next request includes Age and can already be large under Tiered Cache.
|
[AI] Thanks @zaidoon1 — all 4 comments applied in
|
zaidoon1
left a comment
There was a problem hiding this comment.
Two small ones on the new bullet, neither blocking.
|
|
||
| - **The response exceeds the [cacheable size limit](/cache/concepts/default-cache-behavior/#cacheable-size-limits) for your plan.** Split the object into smaller assets, or move to a plan with a higher limit. [R2](/r2/) is an origin storage alternative — it does not raise the CDN cacheable size limit. | ||
| - **The origin returned `no-store` or bare `private` in a `Cloudflare-CDN-Cache-Control` or `CDN-Cache-Control` header.** Cloudflare evaluates these headers ahead of `Cache-Control`, in the precedence `Cloudflare-CDN-Cache-Control` > `CDN-Cache-Control` > `Cache-Control`. An origin returning `Cache-Control: public, max-age=3600` together with `CDN-Cache-Control: no-store` produces `BYPASS`. Cloudflare does not forward `Cloudflare-CDN-Cache-Control` to the client, so it is not visible in the response — if you see `BYPASS` without a `CDN-Cache-Control` header, check what the origin actually sent, or have the origin drop the header and retest. A [Cache Rule](/cache/how-to/cache-rules/) with an [Edge Cache TTL](/cache/how-to/cache-rules/settings/#edge-ttl) setting that ignores origin cache-control overrides both directives. Refer to [CDN-Cache-Control](/cache/concepts/cdn-cache-control/) for the precedence rules. | ||
| - **`no-cache`, `max-age=0`, or `s-maxage=0` in these headers do not produce `BYPASS`.** They produce `MISS` on the first request, then [`REVALIDATED`](/cache/concepts/cache-responses/#revalidated) or [`EXPIRED`](/cache/concepts/cache-responses/#expired) — check under [Repeated MISS](#repeated-miss--cacheable-but-not-in-cache) instead. |
There was a problem hiding this comment.
The pointer sends people the wrong way. Repeated MISS is about cache key variance and eviction. Someone who lands here sees MISS once and then REVALIDATED on every request after, so that section has nothing for them and they'll go dig through cache key config for a response that's caching fine.
"Confirm the response reaches cache" is the right target, its second bullet already covers the REVALIDATED and EXPIRED outcome. Or drop the pointer, since the bullet already says what to expect.
|
|
||
| - **The response exceeds the [cacheable size limit](/cache/concepts/default-cache-behavior/#cacheable-size-limits) for your plan.** Split the object into smaller assets, or move to a plan with a higher limit. [R2](/r2/) is an origin storage alternative — it does not raise the CDN cacheable size limit. | ||
| - **The origin returned `no-store` or bare `private` in a `Cloudflare-CDN-Cache-Control` or `CDN-Cache-Control` header.** Cloudflare evaluates these headers ahead of `Cache-Control`, in the precedence `Cloudflare-CDN-Cache-Control` > `CDN-Cache-Control` > `Cache-Control`. An origin returning `Cache-Control: public, max-age=3600` together with `CDN-Cache-Control: no-store` produces `BYPASS`. Cloudflare does not forward `Cloudflare-CDN-Cache-Control` to the client, so it is not visible in the response — if you see `BYPASS` without a `CDN-Cache-Control` header, check what the origin actually sent, or have the origin drop the header and retest. A [Cache Rule](/cache/how-to/cache-rules/) with an [Edge Cache TTL](/cache/how-to/cache-rules/settings/#edge-ttl) setting that ignores origin cache-control overrides both directives. Refer to [CDN-Cache-Control](/cache/concepts/cdn-cache-control/) for the precedence rules. | ||
| - **`no-cache`, `max-age=0`, or `s-maxage=0` in these headers do not produce `BYPASS`.** They produce `MISS` on the first request, then [`REVALIDATED`](/cache/concepts/cache-responses/#revalidated) or [`EXPIRED`](/cache/concepts/cache-responses/#expired) — check under [Repeated MISS](#repeated-miss--cacheable-but-not-in-cache) instead. |
There was a problem hiding this comment.
"these headers" only resolves if you've read the bullet above, which won't happen for someone scanning. Worth naming them here. Helps twice over, since a "not this" item sitting in a list of BYPASS causes is easy to skim wrong.
Two small follow-ups to #32423.
What
cache-responses.mdxandinvestigating-uncached-responses.mdxdocumentingCloudflare-CDN-Cache-ControlandCDN-Cache-Control, with the precedence chainCloudflare-CDN-Cache-Control>CDN-Cache-Control>Cache-Control. Uses a concrete example so the reader sees that a cacheableCache-Controlalongside a non-cacheableCDN-Cache-Controlstill producesBYPASS.Agecallout incache-responses.mdxand the corresponding HIT-verification bullet in the troubleshooting page. Both currently mentionCacheTieredFill=true, which is anhttp_requestsLogpush field, not an HTTP header — readers cannot observe it on a response.Why
Neither BYPASS list mentioned
Cloudflare-CDN-Cache-ControlorCDN-Cache-Control. When bothCache-ControlandCDN-Cache-Controlare set,CDN-Cache-Controlwins per CDN-Cache-Control, so a reader debugging aBYPASSwhile only looking atCache-Controlhas no signal from these pages that another header is in play.@zaidoon1 for review.