Storage scope is not an identity system. Cache-Control: private tells a conforming shared cache not to store a response; it does not teach a browser which application account owns the bytes. This distinction matters when a person logs out, another account logs in, and the same browser requests the same URL again.

Not all cacheable should be shared

An unqualified private directive permits storage by a single-user cache, subject to other caching requirements. Browsers are a common example, not the only implementation. public explicitly permits caching in circumstances that might otherwise prohibit it, including some responses to authorized requests. Public is not required on every already-cacheable response, and it never means that every possible cache must store the response.

The unqualified forms of no-cache and no-store answer different questions. No-cache allows storage but requires successful validation before reuse. No-store prohibits HTTP cache storage and subsequent reuse, for both shared and private caches. Neither is encryption, an access-control check, or a guarantee that application logs and previously saved copies have disappeared. This article uses unqualified directives, not the less widely implemented forms listing individual field names.

Freshness is separate again. A five-second shared lifetime still permits five seconds of incorrect disclosure if the wrong person’s response is stored under a shared key. Shortening max-age cannot repair an identity boundary. Conversely, s-maxage controls shared-cache freshness only; it is not a synonym for private.

When it breaks

Cookies do not automatically prohibit shared caching under HTTP’s general rules. Authorization has specific shared-cache restrictions, but directives such as public can explicitly permit storage. Adding public to improve a hit ratio can therefore undo a protection the application was relying on. Review the actual response body and authentication path before changing that directive.

Vary refines request matching; it does not grant permission to store. Adding Vary: Cookie cannot make a private response eligible for a shared cache. Within a private cache it can distinguish differing Cookie fields, but that is still field matching, not an authorization decision. A token that remains unchanged while permissions change illustrates why matching headers alone cannot establish current access rights.

For sensitive account data, select an explicit response policy and test account transitions. Include logout, switching accounts in one browser profile, and changes in permissions. Keep actual secrets out of diagnostic captures. An origin header inspection alone cannot prove that every proxy on the deployed path honored the policy.

Check it quickly

Use Node.js and npm; this experiment was run with Node 26.0.0 and http-cache-semantics 4.2.0. Install the pinned policy library in a temporary directory, then execute the JavaScript block with NODE_PATH="$cache_lab/node_modules" node. Installation needs the network; the fixture itself makes no requests and contains only invented cookie and authorization values.

cache_lab=$(mktemp -d)
npm install --prefix "$cache_lab" --ignore-scripts --no-audit --no-fund --package-lock=false http-cache-semantics@4.2.0
const assert = require("node:assert/strict");
const CachePolicy = require("http-cache-semantics");

const request = {
  url: "https://example.test/account",
  method: "GET",
  headers: { cookie: "account=alpha" },
};
const cases = [
  ["private, max-age=300", [false, false], [true, true]],
  ["public, max-age=300", [true, true], [true, true]],
  ["no-cache, max-age=300", [true, false], [true, false]],
  ["no-store, max-age=300", [false, false], [false, false]],
];
for (const [directive, sharedResult, privateResult] of cases) {
  for (const shared of [true, false]) {
    const policy = new CachePolicy(request, {
      status: 200,
      headers: { "cache-control": directive },
    }, { shared });
    const result = [policy.storable(),
      policy.storable() && policy.satisfiesWithoutRevalidation(request)];
    assert.deepEqual(result, shared ? sharedResult : privateResult);
    console.log(shared ? "shared" : "private", directive, result);
  }
}

const authorized = { ...request, headers: { authorization: "Bearer example" } };
for (const [directive, expected] of [
  ["max-age=300", false], ["public, max-age=300", true],
]) {
  const policy = new CachePolicy(authorized, {
    headers: { "cache-control": directive },
  }, { shared: true });
  assert.equal(policy.storable(), expected);
  console.log("authorized shared:", directive, expected);
}

const otherAccount = { ...request, headers: { cookie: "account=beta" } };
for (const vary of [false, true]) {
  const headers = { "cache-control": "private, max-age=300" };
  if (vary) headers.vary = "Cookie";
  const policy = new CachePolicy(request, { headers }, { shared: false });
  assert.equal(policy.satisfiesWithoutRevalidation(otherAccount), !vary);
  console.log("different cookie, Vary enabled:", vary, "reuse:", !vary);
}

Each result pair means storage eligibility followed by reuse without validation. Private yields false/false in shared mode and true/true in private mode. Public yields true/true in both. No-cache yields true/false, while no-store yields false/false. The storage check deliberately comes first: callers must not insert forbidden entries into a cache and rely on a later lookup to repair that mistake.

The Authorization checks show the explicit public opt-in changing shared storage eligibility from false to true. They do not establish that any real personalized response is safe to share. Finally, changing the synthetic cookie still matches the private entry without Vary, but stops matching with Vary. That is the concrete counterexample to treating private as account isolation.

This is a policy-library experiment, not a browser, CDN, or logout implementation. Production evidence still needs two distinct accounts, observable origin requests, and body assertions through the real delivery path. Choose that acceptance test before tuning freshness or measuring the hit ratio.

Sources