Cookies
By default, cookies take no part in caching — in either direction. For cacheable (GET/HEAD) requests:
- The
Cookierequest header is removed before the handler runs, so the handler cannot produce cookie-specific output that gets cached and served to someone else. - Cookies never vary the cache key.
- Any
Set-Cookieon the handler's response is removed before the response is cached or returned.
This keeps one visitor's cookie — a session ID, say — from ever reaching another visitor, whether through a later cache hit or through concurrent requests sharing one response.
You can opt request cookies back in per name with allowCookies, or opt in the whole raw header with varies: ["cookie"]. Response cookies have no opt-in: Set-Cookie never survives on a cacheable route. See Setting cookies.
const page = defineCachedHandler(
async (event) => {
event.req.headers.get("cookie"); // always null for cached GET/HEAD requests
return new Response(await renderPage(), {
headers: { "content-type": "text/html" },
});
},
{ maxAge: 60 },
);Note
This applies only to cacheable requests. Methods that bypass the cache — POST, PUT, and requests excluded by shouldBypassCache — reach the handler unchanged: cookies, headers, query, and body intact, and their Set-Cookie passes through untouched.
#Allowing specific cookies
allowCookies lists the incoming cookie names that participate. For those names only:
- They stay in the
Cookieheader the handler receives. - They vary the cache key. The subset is sorted, so cookie order does not matter (as with
allowQuery). Only the allowed subset is keyed — never the full raw header.
All other cookies are still removed from the request, and the response is cached as usual.
const page = defineCachedHandler(myHandler, {
maxAge: 300,
allowCookies: ["theme"], // theme=dark and theme=light cache separately; every other cookie is ignored
});Names are case-sensitive. allowCookies overrides varies: ["cookie"] — if you set both, the allowlist controls the key and the filtered header the handler sees; drop the broad varies entry. Both forms emit the Vary: Cookie header described below.
Important
Allow only cookies whose values select a shared representation — a theme or locale preference. Never allow a per-user secret: every client sending the same value shares one entry.
#Varying by the whole Cookie header
Listing cookie in varies without an allowlist is the broad form of the same opt-in — like listing authorization instead of setting allowAuthorization. The raw Cookie header then:
- joins the cache key as its full unmodified value,
- reaches the handler unchanged,
- is advertised as
Vary: Cookie.
const page = defineCachedHandler(
(event) => renderFor(event.req.headers.get("cookie")), // the full raw header
{ maxAge: 60, varies: ["cookie"] },
);Warning
This can mean one cache entry per visitor. Any analytics, consent, or A/B cookie produces a distinct raw header and therefore a separate variant, and the cache can no longer combine those requests. Prefer allowCookies to key on the few names that select a representation; use varies: ["cookie"] only when the handler genuinely needs the full header.
#allowCookies emits Vary: Cookie
Setting allowCookies adds Cookie to the response Vary header. The ocache key uses a hash of the allowed subset, but Vary can only name whole headers — it cannot tell a CDN that only the theme cookie matters.
Cache-Control: max-age=300, s-maxage=300
Vary: CookieThe header describes the response correctly, but it has a cost:
Warning
Vary: Cookie collapses hit rates in CDNs and shared proxies. Almost every visitor carries some cookie, so each distinct Cookie header becomes a separate downstream variant and shared caching nearly stops. Your ocache entries are unaffected — they key only on the allowed subset — but the cache in front of ocache suffers.
Before ocache v1, this header was omitted: a cookie-keyed route advertised s-maxage without Vary, CDNs cached the response, and one visitor's variant was served to everyone. Upgrading can lower your downstream hit rate — the old rate was higher because it was serving wrong content.
If you need a high downstream hit rate, don't key by cookie:
- Move the choice into the URL — a
?theme=darkparameter withallowQuery, or a separate path. URLs vary the key without touchingVary, so CDNs can cache each variant. - Or keep the cookie-keyed ocache entry but suppress the synthesized downstream freshness lifetime with
sendCacheControl: false. Note this does not emitno-storeand does not guarantee that browsers or CDNs refuse storage.
#Setting cookies
allowCookies never affects the response. A Set-Cookie header does not survive on a cacheable (GET/HEAD) route — it is removed from the stored entry and from the response the caller receives, including the caller whose request produced it.
There is no way to turn this off. A cached response is shared: replayed to later requests on the same key and returned to concurrent callers that shared one handler call. A cookie minted in that response would reach people it was not minted for, and an opt-in would make that leak one option away:
// Even with allowCookies: ["sid"], the Set-Cookie below never reaches any caller.
const page = defineCachedHandler(
() => new Response("hello", { headers: { "set-cookie": `sid=${newSessionId()}` } }),
{ maxAge: 60, allowCookies: ["sid"] },
);
// First visitor: MISS, no Set-Cookie.
// Second visitor: HIT (same key — neither had a `sid`), no Set-Cookie.
// Previously both received `sid=<the first visitor's id>` — a session-fixation hole.To set a cookie, use a request that bypasses the cache. A POST — or any request excluded by shouldBypassCache — reaches the handler unchanged, and its Set-Cookie passes through untouched:
const login = defineCachedHandler(
async (event) => {
if (event.req.method === "POST") {
// Bypassed: this Set-Cookie is neither stored nor shared.
return new Response("ok", {
headers: { "set-cookie": `sid=${await startSession(event)}; HttpOnly; Path=/` },
});
}
return new Response(await renderLoginPage());
},
{ maxAge: 60 },
);#Caveats
- Custom
getKey. As withallowQuery, a customgetKeyfully controls the cache key — allowed cookies no longer vary it automatically. If the output depends on a cookie, include that cookie in yourgetKey. TheCookieheader the handler receives is still filtered to the allowlist. - Entries written by older versions. ocache refuses to serve a stored response that contains
Set-Cookie. An older version that kept allowed cookies may have written such entries; after upgrading, they cannot be replayed from existing storage.