Internet-Draft M. Toomim Updates: 9111 (if approved) Invisible College Intended status: Proposed Standard 2 November 2026 Expires: 6 May 2027 Caching for HTTP Subscriptions draft-toomim-httpbis-braid-subscriptions-caching-00 Abstract Without a subscription, a cache can serve a stored response only while the response is fresh, and then has to ask the origin server whether the resource has changed. So a cache serves a resource that changes often either stale, or with a request to the origin server each time. This document defines how a cache subscribes to the resource, as any HTTP client can. The cache stores each response in the subscription as the response to a GET. While the subscription is continuously validating, the cache knows at every moment that its copy is current, so it can respond to requests with the copy, without asking the origin server, even for responses marked no-cache. When validation stops, the copy ages, and HTTP's rules for stale responses apply. A cache can also serve many subscribers of its own from one subscription to the origin server, and keep private responses private. This document updates RFC 9111, so that a continuously validating subscription satisfies the no-cache directive. This document is one of the Braid-HTTP extensions, which work alone or together; together they make HTTP a state synchronization protocol. About This Document This note is to be removed before publishing as an RFC. Discussion of this document takes place on the HTTP Working Group mailing list (ietf-http-wg@w3.org). The source of this document, and an issue tracker, are at https://github.com/braid-org/braid-spec. Status of this Memo This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79. Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/. Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress." This Internet-Draft will expire on 6 May 2027. Copyright Notice Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved. This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License. Table of Contents 1. Introduction ...................................................3 1.1. Terminology ..................................................4 2. Caches identify responses by their implied requests ............5 3. Continuously validated responses are always fresh ..............5 4. Responses begin aging again when validation discontinues .......6 5. Caches can fan out one upstream subscription to many clients ...7 5.1. Requests for the same variant can share a subscription .......7 5.2. Caches subscribe upstream ....................................8 5.3. Subscribers receive catch-up, then changes ...................9 6. Implementation Status .........................................10 7. Security Considerations .......................................10 8. IANA Considerations ...........................................11 9. References ....................................................11 9.1. Normative References ........................................11 9.2. Informative References ......................................11 1. Introduction Without a subscription, an HTTP cache serves a stored response only while it is fresh (Section 4.2 of [RFC9111]), and then asks the origin server whether the resource has changed. For example, imagine a newspaper's website on election night, as in [SUBSCRIPTIONS], with a results page at /election that changes all evening. The results change too often for any freshness lifetime to suit them. With "Cache-Control: max-age=0", the paper's CDN has to validate its copy with the origin server for every reader's reload. With a longer lifetime, readers see stale results, unless the paper purges the CDN's copy at each change. A purge and the refetch that follows it take one and a half round trips. The examples in this document follow the paper's CDN through the night. With a subscription, the CDN subscribes to /election once, and the origin server sends it each change in half a round trip. While that subscription is continuously validating ([SUBSCRIPTIONS], Section 5), the CDN's copy is validated at every moment. So the CDN responds to each reader's plain GET with its copy, without asking the origin server: Request: GET /election Response: HTTP/1.1 200 OK Version: ("2") Content-Type: text/plain Cache-Control: max-age=0, stale-if-error=86400 Age: 0 Content-Length: 21 Bob leads, 51% to 49% The response has "Age: 0", since the CDN's copy was validated at the moment the CDN responded. The CDN also serves its subscription to every reader who subscribes through it, so the origin server holds one subscription for each edge of the CDN, however many readers the edge has. Without subscriptions, a site has to choose how to serve a page that changes. Through a CDN, the page loads fast, and puts little load on the origin server, but it shows a change only when it polls. Over a WebSocket, it shows each change at once, but it loads its data from the origin server, which holds a connection for every reader. If the page subscribes through the CDN, it gets all three: it loads as fast as a cached page, the origin server holds one subscription for each edge, and each change arrives as fast as over a WebSocket (Section 6). This document defines how a cache stores the responses in a subscription (Section 2), how a validated copy satisfies HTTP's caching rules (Section 3), how the copy ages when validation stops (Section 4), and how a cache serves one subscription to many subscribers (Section 5). It updates RFC 9111 [RFC9111] in one respect: a continuously validating subscription satisfies the no-cache directive (Section 3). It builds on Subscriptions [SUBSCRIPTIONS], and on Multiresponse [MULTIRESPONSE], which the stream mode uses. With Versions [VERSIONS], a cache can resume its subscription where it left off, and catch up each subscriber from that subscriber's own version. 1.1. Terminology The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. This document uses these terms from Subscriptions [SUBSCRIPTIONS]: subscription, catch-up, origin status, validated, and continuously validating. It uses implied request from Multiresponse [MULTIRESPONSE], and current version and variant key from Versions [VERSIONS]. It uses the terms of HTTP Caching [RFC9111], such as stored response, fresh, stale, and shared cache. It also uses these terms: Upstream subscription: A cache's own subscription to the server that it forwards requests to, which can be the origin server or another cache. Subscription key: A subscription's variant key ([VERSIONS], Section 2.1.1), which is made of its method, its target URI, and the request headers that select the variant. Subscriptions with the same key can share one upstream subscription. 2. Caches identify responses by their implied requests Each response in a subscription is the response that the server would have sent to the same GET, without the Subscribe and Heartbeats headers, just after the change. That GET is the response's implied request ([SUBSCRIPTIONS], Section 2). So a cache can store each response as it would any response to that GET (Section 3 of [RFC9111]), with the response's own Cache-Control, Date, and ETag headers. For example, when the CDN subscribes to /election, its catch-up holds a snapshot of the results at version 1. That snapshot responds to a plain GET for /election, so the CDN stores it as the response to the plain GET. The response at version 2 responds to a GET with "Parents: ("1")". It is also a snapshot, holding the whole state at version 2, which is what a plain GET would get at that moment. So it replaces the stored response to the plain GET. The cache does not store the multiresponse itself, which has "Cache-Control: no-store" ([MULTIRESPONSE], Section 2.3). It builds multiresponses of its own for its subscribers (Section 5). A change can also arrive as a patch [UPDATES], which holds only part of the state. If the cache can apply the patch, it applies it to its stored snapshot, and stores the result as the response to the plain GET. If it can't, its stored snapshot doesn't hold the current state, so the cache doesn't serve it to a plain GET until the next snapshot arrives. A cache doesn't need to know a resource's Version Type. It compares versions only for equality, and tracks its current version as Versions describes ([VERSIONS], Section 2.2.3). 3. Continuously validated responses are always fresh When a cache validates a stored response, it asks the origin server whether the response is still current, and the origin server responds with a 304 (Not Modified) if it is (Section 4.3 of [RFC9111]). With a continuously validating subscription, the origin server tells the cache the same thing at every moment ([SUBSCRIPTIONS], Section 5). So while the cache's upstream subscription is continuously validating, each request that the cache satisfies with its copy counts as one that it forwarded for validation, and for which it received a successful response. While the copy is validated: - The cache can use the copy to satisfy a request, as it would a stored response that it had just validated (Section 4.3.3 of [RFC9111]), whatever the copy's freshness lifetime. It sends the copy with "Age: 0" (Section 5.1 of [RFC9111]). - The copy satisfies the no-cache response and request directives (Sections 5.2.2.4 and 5.2.1.4 of [RFC9111]), the must-revalidate directive (Section 5.2.2.2), and a request's "max-age=0" (Section 5.2.1.1). - The cache can evaluate a conditional request against the copy, and respond with a 304 (Not Modified) when the request's validators match it (Section 4.3.2 of [RFC9111]). For example, a reader's plain GET for /election reaches the CDN at 03:05, while the CDN's subscription is continuously validating. The results have "max-age=0", so the copy is stale by RFC 9111's count, but it is validated, and the CDN serves it with "Age: 0", as in Section 1. This rule updates RFC 9111. Section 5.2.2.4 of [RFC9111] says that a no-cache response "MUST NOT be used to satisfy any other request without forwarding it for validation and receiving a successful response". With this document, a continuously validating subscription counts as forwarding each request for validation, and receiving a successful response. The origin server consents to this when it grants the subscription and sends an origin status of "up" ([SUBSCRIPTIONS], Section 5.2), promising the cache each change as it happens. Every other directive applies as usual. For instance, a cache doesn't store a response marked no-store, and a shared cache doesn't store one marked private. And if the response's directives require it, a cache still forwards a request with credentials, so that the origin server can authorize the requester (Section 5.1). 4. Responses begin aging again when validation discontinues Validation stops when the cache's upstream server sends an origin status of "down", when its heartbeats stop, or when the upstream subscription ends ([SUBSCRIPTIONS], Section 5). From then on, the copy ages as any stored response does. After a down, its age is the "age" parameter of the down, plus the time since the down arrived. If the heartbeats stopped, or the subscription ended with no down, its age counts from the last moment that the cache heard from its upstream server. The rules for stale responses then apply as usual (Section 4.2.4 of [RFC9111], and [RFC5861]), given two definitions: - Revalidating the copy includes resubscribing, with Parents set to the copy's version. The copy is validated again at that subscription's origin status of "up", and stays validated while the subscription is continuously validating. - When the cache receives an origin status of "down", or finds that its upstream server's heartbeats have stopped, it is disconnected, as Section 4.2.4 of [RFC9111] uses the word, and has encountered an error, as [RFC5861] uses the word. So with stale-while-revalidate, the cache can serve its stale copy to a plain GET at once, while it resubscribes. With stale-if-error, it can serve the copy while its upstream subscription is down, for up to the number of seconds that the directive gives. And must-revalidate forbids both, so if the cache can't validate the copy, it responds with a 504 (Gateway Timeout) (Section 5.2.2.2 of [RFC9111]). For example, the results have "max-age=0, stale-if-error=86400" in their Cache-Control header. At 03:20, the CDN stops hearing from the origin server, and 30 seconds later its upstream subscription times out. It keeps serving the results it has to plain GETs, each with an Age counted from 03:20, and resubscribes with backoff. When the origin server sends an origin status of "up" on the new subscription, the copy is validated again. 5. Caches can fan out one upstream subscription to many clients A cache can serve subscriptions, as well as plain GETs. For each subscriber, it combines what it has stored with what its upstream server sends, into one stream, and the subscriber can't tell that the stream came from a cache. A private cache, such as a browser's, can do this for one user, as when the results are open in several tabs. A shared cache, such as a CDN, does it for all of its users, and the origin server then holds one subscription for each cache, however many subscribers the cache has. 5.1. Requests for the same variant can share a subscription Two subscribers ask for the same variant of the same resource when their requests have the same target URI, and match in every header that the origin server lists in its Vary header (Section 12.5.5 of [RFC9110]). Their requests then have the same subscription key, and a cache can serve both from one upstream subscription. It sends the headers that select the variant in its own upstream request. A cache takes more care with requests that have credentials. It treats both the Authorization and the Cookie headers as credentials. RFC 9111 names only Authorization (Section 3.5 of [RFC9111]), but caches already treat Cookie the same way by convention, since a response to a request with a cookie is often personal. A cache shares a response to a request with credentials only as the response's directives allow: - If the response is public (Section 5.2.2.9 of [RFC9111]), and has none of no-cache, must-revalidate, or s-maxage, the credentials don't matter for it, and the cache shares it with any subscriber. - If the response has must-revalidate or s-maxage, or is public with no-cache, the cache shares it, but first forwards each new subscriber's request, with that subscriber's credentials, so that the origin server can authorize the subscriber (Section 3.5 of [RFC9111]). - Otherwise, as when the response is private, or has none of these directives, the cache relays the subscription to its subscriber one to one, and shares it with no one else. For example, a logged-in reader requests her page of the races she follows, at /election/my-races, with a cookie, and the origin server marks the response private. The CDN relays that reader's subscription to the origin server, one to one. If the upstream server marks a later response private, or no-store, the cache stops sharing the resource. It ends the subscriptions that it shared, and relays each new subscription one to one. 5.2. Caches subscribe upstream A cache opens its upstream subscription with Parents set to its current version, when it has one, so that each response it gets applies to what it holds. It lists the patch formats and range units that it can apply ([UPDATES], Section 2), so that the upstream server sends it only patches that it can use. A cache can add a Subscribe header to any GET that it forwards upstream. If the resource has no subscriptions, the upstream server responds to the GET as usual, and the request costs nothing extra. If it has, the upstream server grants a subscription, and the catch-up responds to the GET that the cache forwarded. A "Vary: Subscribe" header in a response is also a sign that the resource has subscriptions ([SUBSCRIPTIONS], Section 3). If the cache loses its upstream subscription, it sends an origin status of "down" to each of its subscribers, and resubscribes with backoff, from its current version. Its subscribers stay connected meanwhile. In a chain of caches, each one recovers this way on its own. If the upstream server responds to the cache's subscription request without granting a subscription, the cache acts on the response: - A 404 (Not Found) or a 410 (Gone) says that the resource isn't there, so the cache sends it on to its subscribers, as a response. - A 5xx status code, or a 429 (Too Many Requests), says that the upstream server can't serve the subscription now. The cache sends a down to its subscribers, and retries with backoff, waiting at least as long as any Retry-After header says. - A 200 (OK) with no Subscribe header says that the upstream server doesn't offer a subscription now. The cache sends that response to its subscribers as a snapshot, if it differs from what they hold, and then ends their subscriptions, since it can't keep them validated. A cache can keep its upstream subscription open after its last subscriber leaves, so that its copy stays validated for plain GETs. 5.3. Subscribers receive catch-up, then changes For each subscriber, the cache sends a multiresponse of its own. Its header section is the one from the cache's upstream response, without a Current-Version header until the cache knows its current version. Then the cache sends the subscriber a catch-up, from the subscriber's Parents to the cache's current version. The catch-up is empty if the subscriber already holds that version. Otherwise, it holds the patches that the cache has stored since the subscriber's version, or a snapshot. The cache doesn't have to wait for its upstream server. If the cache could serve its stored copy to a plain GET under RFC 9111 and RFC 5861, it can send that copy first, with its Age, and with no Current-Version, and then send whatever its upstream server sends. A subscriber can name a version that the cache doesn't have, as when the subscriber was last connected through another edge of the CDN, which was further along. Then the cache makes sure that its upstream subscription is open, and watches it for that version. Meanwhile, it asks the origin server where the subscriber's version stands, with a GET that has "Method-Override: HEAD" ([MULTIRESPONSE], Section 5), Parents set to the cache's current version, Version set to the subscriber's, and the subscriber's headers that select the variant. The cache acts on the first response: - A 432 (Version Not Found) says that the origin server doesn't have the version either, so the cache responds to the subscriber with a 432 ([VERSIONS], Section 2.4). - A 304 (Not Modified) says that the subscriber's version is among the ancestors of the cache's version ([VERSIONS], Section 2.3.5). So the subscriber is behind the cache, and a snapshot catches it up. - If the subscriber's version arrives on the upstream subscription, or the origin server's 209 lists the versions between the cache's version and the subscriber's, then the subscriber is ahead of the cache. The cache waits for those versions, and then catches the subscriber up as usual. - If the response says none of these, or none comes in time, the cache forwards the subscriber's request upstream. After the catch-up, the cache sends the subscriber each response that its upstream server sends, as it arrives. It sends origin status as Section 5.2 of [SUBSCRIPTIONS] describes, and heartbeats of its own, at the interval it agreed to with the subscriber. The subscription ends when the subscriber leaves, or when the cache ends it, as it can when the subscriber stops reading. 6. Implementation Status This section records the known implementations of this document, as [RFC7942] describes. It is to be removed before publishing as an RFC. The Braid-HTTP polyfill for Cloudflare [CDN-POLYFILLS] is a cache for subscriptions that runs on Cloudflare's network, using only its public APIs. It holds one upstream subscription for each resource, and stores its responses in the cache of each Cloudflare data center that has readers. It serves plain GETs from a validated copy, serves subscriptions from its copy and its upstream subscription, and adds Subscribe to the GETs that it forwards upstream. It honors private, no-store, stale-while-revalidate, and stale-if-error, and it relays every request with an Authorization or Cookie header one to one, whatever the response's directives. In September 2026 we measured the polyfill on Cloudflare's network, from test machines in several regions [MEETING-144]. An edit reached the edge cache one round trip sooner than with a purge and a refetch, which made it 41% to 49% faster. A page that subscribed through the polyfill loaded about 9 times faster than a page that loaded its data over a WebSocket, and showed each edit as quickly as the WebSocket page did. We also tested subscriptions through Cloudflare's own cache, with tests that we wrote for the HTTP cache-tests engine [CACHE-TESTS]. Cloudflare passed each 209 through, streaming each response as the server sent it. 7. Security Considerations When a cache serves one subscription to many subscribers, it shares one response among many users. So it follows the rules of RFC 9111 for shared caches (Section 3.5 of [RFC9111]), and the rules for credentials in Section 5.1. When a cache relays a private subscription one to one, it shares nothing with other users. When a cache serves requests from a validated copy, the origin server stops seeing those requests, and loses the signal that each one gave it, as for logging, or for counting views. If an origin server needs to see every request, it can mark its responses private, or decline to grant subscriptions. When a subscriber's Parents names a version that the cache doesn't have, the cache asks the origin server where that version stands (Section 5.3). A client could use this to make the cache send many requests, so a cache can limit how often it asks for each client. A cache trusts the origin status that its upstream server sends, as it trusts that server's responses, and its subscribers trust the cache the same way ([SUBSCRIPTIONS], Section 9). 8. IANA Considerations This document has no IANA actions. 9. References 9.1. Normative References [MULTIRESPONSE] Toomim, M., "Multiresponses for HTTP", draft-toomim-httpbis-braid-multiresponse-00, work in progress, November 2026. [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997. [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, May 2017. [RFC9110] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Semantics", STD 97, RFC 9110, June 2022. [RFC9111] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Caching", STD 98, RFC 9111, June 2022. [SUBSCRIPTIONS] Toomim, M., "Subscriptions for HTTP", draft-toomim-httpbis-braid-subscriptions-00, work in progress, November 2026. 9.2. Informative References [CACHE-TESTS] Nottingham, M., "HTTP Caching Tests", . [CDN-POLYFILLS] braid.org, "Braid Polyfills for CDNs", . [MEETING-144] Toomim, M., "Braided CDN Polyfills", Braid Meeting 144, September 2026, . [RFC5861] Nottingham, M., "HTTP Cache-Control Extensions for Stale Content", RFC 5861, May 2010. [RFC7942] Sheffer, Y. and A. Farrel, "Improving Awareness of Running Code: The Implementation Status Section", BCP 205, RFC 7942, July 2016. [UPDATES] Toomim, M., "Replayable Updates for HTTP", draft-toomim-httpbis-braid-updates-00, work in progress, November 2026. [VERSIONS] Toomim, M., "Resource Versioning for HTTP", draft-toomim-httpbis-braid-versions-00, work in progress, November 2026. Author's Address Michael Toomim Invisible College, Berkeley 2053 Berkeley Way Berkeley, CA 94704 EMail: toomim@gmail.com Web: https://invisible.college/@toomim