HTTP Caching and Revalidation Correctness: A Proposed Axis for What AI App Builders Declare About Reusing Their Own Responses (September 2026)
A candidate BuilderProof axis scoring what a generated application declares about who may store its responses, for how long, keyed on what, and how staleness is detected. The HTTP caching specification's shared-cache protection is keyed on the Authorization header, which is not how most generated apps authenticate. Seven weighted signals, four postures, a nine-step protocol, no scores.
On this page
An AI app builder will generate a dashboard, a profile page, an order list and an admin view, and every one of them will be correct the first time you look at it. The question this axis proposes to measure is what happens the second time, and to the second person. Specifically: what does the emitted code say about how long its responses may be reused, who may reuse them, and how anyone finds out the data has changed. We are not scoring builders today. We are publishing a candidate axis, its rubric, its posture levels and a reproduction protocol, and opening all of it for comment before it enters the composite.
Quick Answer
HTTP caching and revalidation correctness is a proposed BuilderProof benchmark axis, drafted September 3, 2026, that scores what an AI app builder's emitted application declares about the reuse of its own responses. It is read from the deployed artifact's response headers and its source, not from the preview. The rubric weights seven signals: whether user-specific responses are excluded from shared caches, whether freshness is stated explicitly rather than left to heuristics, whether validators are emitted so a client can revalidate cheaply, whether the cache key accounts for what actually varies the response, whether a write invalidates the reads it affects, whether immutable assets are marked as such, and whether the directives are coherent across the surface. The failure mode it targets is not slowness. It is one reader being served another reader's data by an intermediary that was never told the response was personal, and a reader being shown data that a write already replaced. The specification's own protection against the first case is keyed on the Authorization request header, which is not how most generated applications authenticate. This page is an axis proposal open for community edits, not a leaderboard.
BuilderProof is an independent, community-editable benchmark for AI app builders. It accepts no vendor payment or sponsorship. This post proposes a new axis, defines how it would be scored, and cites primary sources read on September 3, 2026. It does not score any builder on this axis yet, and it does not name a winner.
Why caching correctness deserves its own axis
Caching is usually filed under performance, which is why it has never been scored as correctness anywhere we can find. That filing is a mistake, and the reason is structural.
A cache is not a speed feature bolted onto a correct system. A cache is a second copy of your data held by a party that did not produce it, for a duration nobody stated, keyed on fields nobody chose. Every one of those three blanks is filled in by default if the application does not fill them in itself. The defaults are not malicious and they are not unreasonable. They are simply not decisions the application made, and on a surface carrying per-user data, one of those undeclared defaults is a data-exposure bug and another is a correctness bug.
The relevant guidance is old, settled and unambiguous. RFC 9111, the current HTTP caching specification, distinguishes the two kinds of cache in its first section: a "shared cache" is one "that stores responses for reuse by more than one user", usually deployed as part of an intermediary, while a "private cache" is "dedicated to a single user" and is usually a component of the user agent. That distinction is the whole axis in one sentence. Everything below is about whether the emitted application ever tells the difference to anyone.
What "caching and revalidation correctness" means here
The axis scores the deployed artifact and its source, not the vendor's marketing and not the chat experience. It asks, of the untouched build:
For each response the application emits, does the application state who may store it, for how long, keyed on what, and how a stored copy is checked for staleness, or does it leave every one of those to a default it did not choose?
Four boundaries keep this honest.
- It measures declaration, not speed. An application that declares
no-storeon everything is slow and correct, and it scores well here. Performance is a separate axis and always was. - It measures the emitted surface. Static asset caching handled entirely by the hosting platform is recorded as inherited, not as authored, in the same way our other axes separate what the builder wrote from what the framework supplied.
- It measures the deployed artifact. Header behaviour cannot be read reliably from source alone, because a platform may add, strip or override. The protocol below requires a deploy.
- It is not an architecture preference. Aggressive edge caching with correct scoping and no caching at all are both legitimate postures. The axis asks whether the choice was made and applied, not whether it matches ours.
The finding: the specification's safety net is keyed on a header these apps do not use
This is the part we think is genuinely unmeasured, and it comes from reading two primary documents against each other rather than from any new experiment.
RFC 9111 section 3.5 contains the protection that most people believe covers them. It states that a shared cache "MUST NOT use a cached response to a request with an Authorization header field ... to satisfy any subsequent request" unless the response carries a directive explicitly permitting shared storage. That is a strong, mandatory guarantee, and it is the reason experienced engineers rarely worry about proxies leaking authenticated pages.
Read the condition precisely. The guarantee attaches to requests carrying an Authorization header field. It does not attach to requests carrying a session cookie.
That distinction did not matter much when the specification was written, because HTTP authentication meant the Authorization header. It matters enormously now, because the dominant authentication pattern in the applications these builders emit is a session cookie: the platform sets a cookie, subsequent requests carry it, and middleware reads it. Such a request carries no Authorization header at all, so section 3.5 is simply not engaged. The mandatory protection is present, correct, and pointed at a header the application never sends.
The specification is explicit that the adjacent intuition is also wrong. In its security considerations, under caching of sensitive information, RFC 9111 states plainly: "Note that the Set-Cookie response header field does not inhibit caching; a cacheable response with a Set-Cookie header field can be (and often is) used to satisfy subsequent requests to caches." It goes on to say that servers wishing to control caching of such responses "are encouraged to emit appropriate Cache-Control response header fields." Encouraged. The burden is on the application, by design, and the specification says so.
So on a cookie-authenticated application, there is no automatic protection from the caching layer. There is only whatever the generated code declared. That is precisely the kind of silent, structural, default-dependent property a benchmark axis exists to measure.
The honest counterweight: platforms compensate, partially
We would be overstating this if we stopped there, because deployment platforms have noticed the same gap and added their own guards, and those guards are real.
Vercel's edge network documentation, which is directly relevant because it is where a Next.js build from v0 lands, enumerates the conditions a response must satisfy to be cached. Among them: the request must use GET or HEAD, the request "doesn't contain Authorization header", the response "doesn't contain the set-cookie header", the response "doesn't contain the private, no-cache or no-store directives in the Cache-Control header", and the response "doesn't contain Vary: * header, which is treated as equivalent to Cache-Control: private".
Two of those go beyond what RFC 9111 requires, and both are protective. Refusing to cache any response carrying Set-Cookie is a direct mitigation of the exact hazard the specification warns about.
But notice which request the Set-Cookie guard actually catches. It catches the response that establishes a session. It does not catch the responses that use one, and those are the overwhelming majority. In steady state a logged-in user sends a Cookie header on every request and the application sends no Set-Cookie back, because the session already exists. Such a response satisfies every listed condition. If the application has also declared a shareable freshness lifetime, that response is cacheable at the edge, and the same documentation notes that the CDN "already includes the Accept and Accept-Encoding headers as part of the cache key by default", a list that does not include Cookie.
That is the residual gap, stated as narrowly as the evidence supports: the platform guard fires on the request that sets a cookie, and the specification guard fires on requests that use Authorization. Neither fires on the ordinary steady-state request of a cookie-authenticated application. Only the application's own declaration does. Vercel's own documentation agrees about where the line sits, listing CDN caching as not the right fit when "Responses include sensitive user data".
What the framework defaults actually do
We checked the default rather than assuming it, and the default is good. The Next.js caching documentation states that "By default, fetch requests are not cached", and that caching is opt-in via cache: 'force-cache'. A framework that does not cache unless asked cannot produce this defect on its own.
The interesting material is in the opt-in and its neighbours, and all three items below are worth a rubric row.
The fetch reference states, under a "Good to know" note, that "Caching is opt-in. Set cache: 'force-cache' to cache any request, including POST and requests that send authorization or cookie headers." The escape hatch is documented as having no authentication guard, in the vendor's own words. It also notes that a cached request "matches on its URL, method, headers, and body", so the framework's own data cache does key on headers, which is a genuine mitigation at that layer and worth crediting.
The route segment option dynamic = 'force-static' is documented as forcing "cookies, headers() and useSearchParams() to return empty values". A generated page that reads a session cookie and is then marked static does not fail loudly. It renders as though nobody is logged in.
And a conflicting pair is documented as being silently discarded: "Conflicting options such as { revalidate: 3600, cache: 'no-store' } are not allowed, both will be ignored", with a warning printed only in development. An operator who wrote no-store and shipped it may have neither.
Next.js 16 has since added a 'use cache: private' directive for exactly this problem, documented as allowing a cached scope to access cookies() and headers() while "results are never stored on the server, they're cached only in the browser's memory and do not persist across page reloads". That is the framework encoding the shared-versus-private distinction as a first-class primitive, which is a good sign for where this is heading and a reasonable thing for a rubric to reward.
The proposed rubric
Seven signals, weighted to 100, scored from the deployed artifact's response headers and the emitted source. Weights are a proposal and are the part we most want argued with. We have written the third column as what a failing case looks like, because a failing case is easier to check against your own build than an abstract ideal.
Scroll to see more
| Signal | Weight | What a failing case looks like |
|---|---|---|
| Private-response scoping | 24 | A response containing one user's data is returned with a shareable freshness lifetime and no private, no-store or no-cache directive, so an intermediary is permitted to serve it to a second user |
| Explicit freshness declaration | 18 | Responses carry no Cache-Control and no Expires, leaving a cache free to assign a heuristic lifetime the application never chose and cannot predict |
| Validator emission and revalidation | 16 | No ETag and no Last-Modified on any response, so a client holding a stale copy has no way to ask whether it changed and must refetch the whole body or guess |
| Cache-key completeness | 14 | The response varies by session, locale or role, but no Vary header names the field that varies it, so one stored entry answers requests that should have received different bodies |
| Post-write invalidation | 13 | A successful write returns 200 and the list view continues serving the pre-write copy until an arbitrary timer expires, with no revalidation, tag purge or path invalidation wired to the mutation |
| Immutable asset marking | 8 | Fingerprinted build assets that can never change are served without a long lifetime or immutable, so every visit re-requests bytes that are already correct |
| Directive coherence | 7 | Different routes on the same surface use contradictory or nonsensical directive combinations, or combinations the framework documents as silently ignored |
Three weighting decisions are worth arguing about now rather than after the axis ships.
Why private-response scoping carries the most. It is the only signal on this list whose failure is a data-exposure defect rather than a staleness defect. Every other row costs correctness or bytes. This one can cost a reader someone else's data, and a rubric that averaged it in with asset fingerprinting would be lying about the stakes.
Why explicit freshness outranks validator emission. A response with no freshness information at all is the one where a cache invents a lifetime, and RFC 9111 permits that: it states a cache "MAY assign a heuristic expiration time when an explicit time is not specified", using other fields such as Last-Modified to estimate one, and that heuristics may only be used on responses without explicit freshness. The specification is clear that an explicit time removes the guesswork, and the guesswork is the thing we want measured.
Why post-write invalidation is a caching row and not a write row. This is the boundary with our concurrent-write axis, and we state it below rather than leaving it implied. The write landing correctly is that axis. The reader still seeing the old value afterwards is this one.
The four postures
Scores roll up into a posture level so the axis reads at a glance without collapsing the detail.
Level 0, Undeclared. No cache directives emitted anywhere by the application. Freshness is whatever the platform default and cache heuristics produce. Nothing distinguishes a public marketing page from a personal order history. This posture is not necessarily broken today, because a platform default may be conservative, but nothing in the artifact makes that a property of the application rather than an accident of where it was deployed.
Level 1, Platform-inherited. Static assets are cached well, because the hosting platform does that automatically and correctly. Dynamic responses remain undeclared. The good behaviour is entirely inherited and would not survive a move to a different host, which is exactly the property our runtime lock-in axis exists to notice.
Level 2, Scoped. The application distinguishes public from personal. Responses carrying user data are marked private or no-store; genuinely public responses carry an explicit lifetime; Vary names the fields that actually vary the body. Reuse is bounded by a decision the application made.
Level 3, Validated and invalidated. Everything in Level 2, plus validators are emitted so clients can revalidate and receive 304 rather than a whole body, and writes invalidate the reads they affect, by tag, by path, or by revalidation, so a reader sees a change without waiting for a timer to expire.
We have no prediction to publish about where the cohort sits, because we have not measured it, and an axis proposal that pre-announced its result would be a poor proposal.
How to reproduce it
The protocol is a header sweep plus a source read plus one write test. It is cheap enough to run against your own build this afternoon, which is the point of a community-editable benchmark.
- Build the reference application from the fixed prompt. The brief describes an application with a public page, an authenticated per-user page, a list view and a form that writes to that list. The prompt says nothing about caching, headers, performance or staleness. Asking for the safeguard would measure prompt compliance, not default posture.
- Deploy it. Header behaviour is a property of the deployed artifact. A local development server is not evidence, and several frameworks behave differently in development by design.
- Record the full response headers for every route, authenticated and unauthenticated, capturing
Cache-Control,Expires,ETag,Last-Modified,Vary,Ageand any platform cache-status header. - Classify each route as public or personal from what the body contains, not from what the route is named.
- For every personal route, check whether anything in the response would stop a shared cache storing it. Record the specific directive, or record its absence.
- Request one unchanged resource twice, sending back any validator received the first time, and record whether a 304 is returned or the whole body is sent again.
- Perform a write, then immediately re-read the affected list, and record whether the new value appears, how long it takes, and whether anything in the response explains the delay.
- Read the source for cache configuration and record it separately from observed headers: fetch cache options, route segment configuration, explicit header setting, and any platform configuration file.
- Repeat on a second independent generation from the same brief and report both scores before averaging, because per-run variance on a property nobody prompted for is itself a finding.
Step 5 is the one most likely to be skipped and the one that carries the highest-weighted signal. Step 7 is the one most likely to produce a surprise, because a stale read after a successful write is the failure users report as "it didn't save" when in fact it saved perfectly.
The named trap: the private-cache illusion
Each axis in this series names the illusion that hides its defect. Tests that pass without asserting are the green-check illusion. A surface whose only consumer wrote itself is the sole-consumer illusion. A short page mistaken for the end of a collection is the first-page illusion.
The trap here is the private-cache illusion, and it follows directly from RFC 9111's own vocabulary.
Every cache you encounter while building an application is a private cache. Your browser cache is dedicated to you. Your development server has one user. Your preview link is opened by you and by two colleagues who are each, individually, a single user with a single browser. In that entire loop there is no cache that stores a response for reuse by more than one person, which means the whole category of shared-cache defects is structurally unreachable by any test performed during development.
The shared cache appears at deployment, and it appears silently. It is the CDN in front of the origin, the reverse proxy in the corporate network, the cache in the mobile carrier's path. None of them announce themselves, none of them appear in the preview, and each of them takes the application entirely at its word about what may be reused and by whom. If the application said nothing, they apply a default it never chose.
This is why the defect survives ordinary testing so reliably. It is not that the developer was careless. It is that the environment in which the application was built contains no instance of the thing that breaks it.
How this relates to our existing axes
The boundaries matter enough to state explicitly, because two neighbouring axes touch this territory and one of them shares a header family with it.
It is not our concurrent-write safety axis, and the cleanest way to see the split is that both axes use conditional requests from the same specification for opposite purposes. RFC 9110 describes If-Match as "most often used with state-changing methods ... to prevent accidental overwrites when multiple user agents might be acting in parallel on the same resource (i.e., to prevent the 'lost update' problem)". That is the write side, and it belongs to that axis, which weights conflict detection by version or timestamp predicate at 12. The same specification describes If-None-Match as "primarily used in conditional GET requests to enable efficient updates of cached information with a minimum amount of transaction overhead", letting a server answer 304 when a stored copy is still current. That is the read side, and it is this axis. Same header family, opposite direction, different failure.
The two scores can move in opposite directions, which is the test we apply before proposing any axis adjacent to an existing one. A build can reach Level 3 on concurrent writes, refusing every stale write with a version predicate and surfacing the conflict properly, and still serve every reader a personal page from a shared cache with a one hour lifetime. The reverse also holds: a build can emit flawless private directives, validators and invalidation on every read path and still lose an update the moment two people edit the same row.
It is not our API-design consistency axis either, and that axis draws the line itself: it measures coherence, not correctness, scoring whether the surface is internally uniform rather than whether it is right. Only one of our seven signals, directive coherence, is a consistency measure, and it carries the lowest weight here for that reason. The distinction is easy to test. A surface that emits the identical Cache-Control: public, max-age=3600 on every route, including the authenticated ones, is perfectly coherent and scores well there. It fails this axis on the highest-weighted signal it has. That axis also covers idempotency in the protocol sense, whether GET is free of side effects and whether PUT and DELETE are repeatable, which is about method semantics and not about response reuse.
It is also not our performance work. Our speed-to-first-paint benchmark deliberately measures a cold run with, in its own words, no warm cache, because it is trying to remove caching as a variable. This axis measures the path that benchmark deliberately excludes.
What we are not claiming
We are not claiming that any builder in the cohort leaks data through a shared cache. We have not scored this axis. The proposal exists precisely so the rubric can be attacked before any number is attached to a vendor name.
We are not claiming that the defect is common. We are claiming that it is undeclared by default, structurally invisible during development, and unmeasured by any benchmark we could find, which is a different and more modest statement.
We are not claiming that aggressive caching is wrong. A correctly scoped public, max-age=31536000, immutable on a fingerprinted asset is excellent practice and scores full marks here.
We are not claiming this is the most important axis. It is a specific, currently unmeasured property whose worst failure is a privacy failure rather than a performance one, and that reclassification is the argument for measuring it.
Limitations and open questions
Observed headers may not be the origin's headers. A platform can add, strip or rewrite directives in transit, which means a header sweep measures the deployed system rather than the generated code alone. Recording source configuration separately, as step 8 requires, is a partial mitigation and not a complete one. Where the two disagree, we would report the disagreement rather than pick a winner.
The public-versus-personal classification is a judgement. Deciding that a route carries user data is usually obvious and occasionally not, for instance on a page that is public but personalised. A rubric that penalises an ambiguous case as though it were an order history is measuring our classification rather than the artifact.
Inherited behaviour is hard to attribute fairly. A builder deploying to a platform with conservative defaults will look better than one deploying to a permissive platform, for reasons that have nothing to do with generated code quality. Separating authored from inherited, as the posture levels attempt, may not be clean enough, and we would like the argument against our current split.
We have not measured the cohort. Everything above is read from specifications and vendor documentation. Documentation is evidence of design intent, not of behaviour on a given run, and the gap between them is exactly what the protocol exists to measure.
The weights are a first draft. In particular, 8 points for immutable asset marking may be too generous for a signal that platforms increasingly handle automatically, in which case the row is measuring the host rather than the builder and should arguably be dropped to zero or moved to the deploy-quality axis.
Corrections, counterexamples from real deployed builds, and rubric edits are welcome. The most useful thing you can send us is a header dump from your own generated application that contradicts what the documentation above implies.
References
- RFC 9111, "HTTP Caching," Internet Standard, June 2022, section 1 shared and private cache definitions, section 3.5 storing responses to authenticated requests, section 4.1 calculating cache keys with the Vary header field, section 4.2.2 calculating heuristic freshness, section 5.2.2.7 the private directive, section 7.3 caching of sensitive information, read September 3, 2026: https://www.rfc-editor.org/rfc/rfc9111.html
- RFC 9110, "HTTP Semantics," Internet Standard, June 2022, section 13.1.1 If-Match and the lost update problem, section 13.1.2 If-None-Match and conditional GET, read September 3, 2026: https://www.rfc-editor.org/rfc/rfc9110.html
- Next.js documentation, "Caching and Revalidating," last updated August 25, 2026, fetch requests not cached by default, force-dynamic and force-static segment options, fetchCache configuration, read September 3, 2026: https://nextjs.org/docs/app/guides/caching
- Next.js documentation, "fetch," API reference, opt-in caching including requests that send authorization or cookie headers, cache matching on URL, method, headers and body, silently ignored conflicting options, read September 3, 2026: https://nextjs.org/docs/app/api-reference/functions/fetch
- Next.js documentation, "use cache: private," directive reference, last updated August 25, 2026, browser-memory-only caching for scopes that read cookies and headers, read September 3, 2026: https://nextjs.org/docs/app/api-reference/directives/use-cache-private
- Vercel documentation, "Vercel CDN Cache," cacheability conditions, default cache key composition, Cache-Control and CDN-Cache-Control header precedence, read September 3, 2026: https://vercel.com/docs/edge-network/caching
Cite this benchmark
BuilderProof editorial team. "HTTP Caching and Revalidation Correctness: A Proposed Axis for What AI App Builders Declare About Reusing Their Own Responses (September 2026)". BuilderProof, September 2026. https://www.builderproof.org/benchmarks/http-caching-revalidation-correctness-axis-proposal-september-2026.
@misc{builderproof-http-caching-revalidation-correctness-axis-proposal-september-2026,
title = {{HTTP Caching and Revalidation Correctness: A Proposed Axis for What AI App Builders Declare About Reusing Their Own Responses (September 2026)}},
author = {{BuilderProof editorial team}},
year = {2026},
month = {sep},
howpublished = {\url{https://www.builderproof.org/benchmarks/http-caching-revalidation-correctness-axis-proposal-september-2026}},
note = {BuilderProof, builderproof.org}
}Frequently asked questions
Is HTTP caching a performance concern or a correctness concern?
Both, and this proposed axis scores only the correctness half. A cache is a second copy of a response held by a party that did not produce it, for a duration the application may never have stated, keyed on fields the application may never have chosen. On a surface carrying per-user data, one undeclared default is a data-exposure defect and another is a staleness defect. Speed is measured by our separate speed-to-first-paint work, which deliberately runs with no warm cache.
Does the HTTP specification stop a shared cache from serving one user's page to another?
Only for requests carrying an Authorization header. RFC 9111 section 3.5 states that a shared cache must not reuse a cached response to such a request unless a directive explicitly permits shared storage. Most applications generated by AI app builders authenticate with a session cookie rather than that header, so the protection is not engaged. RFC 9111 also states directly that the Set-Cookie response header field does not inhibit caching, and encourages servers to emit appropriate Cache-Control fields instead.
Do hosting platforms cover the gap?
Partially, and the residual is narrow. Vercel's CDN documentation lists conditions for caching that include the request having no Authorization header and the response having no Set-Cookie header. The Set-Cookie guard catches the response that establishes a session, not the many later responses that merely use one, and the default cache key includes Accept and Accept-Encoding but not Cookie. In steady state a logged-in request satisfies every listed condition, so the application's own declaration is what remains.
How is this different from the concurrent-write safety axis?
They use conditional requests from the same specification for opposite purposes. RFC 9110 describes If-Match as being used with state-changing methods to prevent the lost update problem, which is the write side and belongs to that axis. It describes If-None-Match as primarily used in conditional GET requests to update cached information efficiently, which is the read side and is this axis. A build can refuse every stale write correctly and still serve a personal page from a shared cache.
Are any builders scored on this axis?
No. This is a pre-registration, not a report. It publishes a rubric, four posture levels and a reproduction protocol so the method can be attacked before any number is attached to a vendor name. BuilderProof publishes no placements on a proposed axis, and this page names no cohort result.
Related benchmarks
Concurrent-Write Safety Posture: A Proposed Axis for What AI App Builders Emit When Two Writes Collide (August 2026)
A candidate BuilderProof benchmark axis that scores whether the code AI app builders emit stays correct when two writes to the same record overlap. Rubric, four posture levels, a reproduction protocol, and an open call for comment.
API-Design Consistency of Emitted Routes: A Proposed Axis for Whether an AI Builder's Endpoints Agree With Each Other (August 2026)
A candidate BuilderProof benchmark axis that scores whether the HTTP surface an AI app builder emits is internally coherent across every endpoint: one error shape, uniform status-code semantics, one addressing scheme, consistent collection semantics, and a machine-readable contract. Divergence-based scoring, four documented postures, a probe-sweep protocol, and an open call for comment.
Does It Answer the First Visitor? A Proposed Axis for Cold Start and First-Request Latency (September 2026)
Every timing a developer has was taken from a system their own presence kept awake. A pre-registered seven-signal axis for what the first visitor after a quiet period actually experiences.