Cookies

By default, cookies take no part in caching — in either direction. For cacheable (GET/HEAD) requests:

  • The Cookie request 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-Cookie on 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 Cookie header 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.

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.

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: Cookie

The 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=dark parameter with allowQuery, or a separate path. URLs vary the key without touching Vary, 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 emit no-store and 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 with allowQuery, a custom getKey fully controls the cache key — allowed cookies no longer vary it automatically. If the output depends on a cookie, include that cookie in your getKey. The Cookie header 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.