Skip to content

The cache was fast. Why did one customer see another customer's orders?

The cache was fast until it wasn't · Part 5 of 6 · 5 min read

Read the worked explanation ↓

A platform serves a personalized order-history endpoint at GET /api/me/orders. Authentication uses a session cookie. A new CDN rule caches JSON responses for 60 seconds under a key built mainly from the URL. A customer from tenant B now sees an order list that belongs to tenant A.

Interview question. Reconstruct how this can happen, specify which responses may be cached at which layer, and propose test cases that prove authorization boundaries survive cache hits. Assume the origin returns Cache-Control: public, s-maxage=60 by mistake and the CDN rule permits caching of this endpoint. This is a fictional misconfiguration, not normal behavior to assume of every CDN.

Think first

GET /api/me/orders uses a cookie session. A custom CDN rule and mistaken s-maxage allow a URL-only cached personalized 200. Explain the cross-tenant exposure, fix the caching policy, and specify tests that exercise the edge after permission changes.

Take a few minutes to form an approach, then compare it with the worked explanation.

Reveal the worked explanation

Two different users made the same cache lookup

The request path looked reasonable during single-user testing. Tenant A called /api/me/orders. The origin authenticated A using its cookie and returned A's order history. The shared edge stored the response under a key equivalent to the public URL.

Two authenticated users collide on a shared URL-only cache key while the correct path bypasses shared caching
Two authenticated users collide on a shared URL-only cache key while the correct path bypasses shared caching

Thirty seconds after A’s response was generated, while it was still within the 60-second freshness lifetime, B made the same GET request. The CDN found a cached response for /api/me/orders and returned it without a new origin authorization check. The cache was efficient. It had grouped requests that were not interchangeable.

The business identity was in the authenticated context, but the cache identity was only the path. That is the fundamental mismatch. A cache key is part of the data-access policy whenever cached responses depend on identity, tenant, permissions, locale, feature flags, or other request context.

Do not summarize this as “CDNs always cache authenticated content.” They should not. RFC 9111 restricts shared caching of requests with an Authorization header unless specific response directives permit it. In this scenario, authentication uses a cookie and the origin has also mistakenly advertised a shared-cacheable response. A Cookie header by itself is not a universal prohibition on caching under HTTP rules. Provider defaults and custom cache rules therefore matter.

What should be cached at all?

For this order-history endpoint, I would choose a safe, simple rule: don't store the personalized response in a shared cache. Set appropriate response directives, such as Cache-Control: private, no-store for especially sensitive histories, and configure the CDN to bypass caching on authenticated account routes. private stops compliant shared caches from storing the response. no-store instructs compliant caches not to store it at all, including private caches. Neither directive replaces access control at the origin.

The page can still be fast. Cache its public static assets at the CDN, cache genuinely public product metadata under public keys, and assemble personal data from a properly authorized endpoint. A customer-specific application cache can be useful if its keys and access path are scoped to the authenticated user and tenant, and if revocation and data freshness have appropriate handling.

Could we put the user ID into the CDN key instead? Technically, partitioned caching may be possible, but the key must be derived from a trusted authenticated identity after authorization, not a caller-supplied userId query parameter. Even then, any change in permissions, account membership, or consent must be reflected in the cache contract. High-cardinality user caching at the edge may offer poor reuse and substantial operational risk for an order-history endpoint.

Similarly, Vary: Cookie can instruct compliant HTTP caches to distinguish responses based on cookie values. It is not a complete security boundary, and including an entire session cookie often destroys reuse. It also does not protect a custom intermediary that ignores the header or forces a cache-key override. I'd rather make the no-shared-cache rule explicit for this sensitive route.

Tests that a single happy-path run will miss

I would run requests in this order against the deployed edge, not just directly against the origin:

A logs in, opens order history
B logs in, opens the identical URL
A logs out, B refreshes
A loses access to tenant A, then retries
An anonymous client opens the same URL

For each response, verify the correct authorization and identity, the status code, the relevant cache directives, and whether the edge reports a hit or bypass. Use two tenants with deliberately distinguishable fixture data. Include the case where a cached 200 is followed by a permission change. A successful origin test proves nothing about an edge rule that can answer without reaching the origin.

I would also test query-string variants, canonicalization, headers, and paths that appear distinct to the application but identical to a custom cache key. Cloudflare's cache-key guidance warns that inputs not represented in the cache key can cause different responses to collide. Avoid using raw secrets or bearer tokens as keys in logs or dashboards.

If this happened in production, stop shared caching of the affected route immediately, purge exposed objects, preserve incident evidence, and assess the exposure through trusted edge logs and application records. A one-minute TTL does not make a cross-customer data leak harmless. Communicate and remediate according to the incident's actual scope.

Interview takeaway: Before discussing a cache hit rate, ask whether two requests are authorized to share a cached answer. For personalized data, correctly isolating the result is more important than making it globally reusable.

A cache can be perfectly isolated and still become a reliability dependency. Suppose the whole cache fleet restarts with no warm entries. How do we bring it back without overwhelming the database?

Go deeper

Shared caches can legitimately serve authenticated requests under carefully designed conditions, such as a trusted identity-aware edge whose data is separated by tenant and permission generation. That is a more complex security design, not an automatic benefit of choosing s-maxage.

Authorization has two dimensions here: the route must establish the caller's permission, and the cache must ensure the response it reuses was generated under compatible permissions. Permission revocation can invalidate earlier keys or move the caller to a new generation, but the revocation source and delivery delay must be part of the guarantee.

Also consider error caching. A stale cached 403 from an old permission state can wrongly deny an authorized user. A cached 200 is more dangerous because it can leak data. Both require explicit policy rather than a global cache-everything rule.

Check your reasoning

  • Identify mismatch between the URL cache key and authenticated response identity.
  • Do not claim all CDNs routinely cache Authorization-bearing responses by default.
  • Specify private/no-store and edge bypass for sensitive personalized endpoints.
  • Explain why simply varying on a user-provided ID or raw token is not a safe design.
  • Test two principals through the actual edge, revocation, anonymous access and cache-hit behavior.

Common mistake: Assuming a successful origin authorization check protects a hit served by an upstream shared cache.

A response contains a user’s order history. Which is the safest baseline caching policy?

Read the answer explanation

Bypass shared caching and apply a private/no-store policy appropriate to sensitivity. — Sensitive personalized responses should not be reused across users by shared intermediary caches.

Apply it elsewhere

An admin changes a user’s role but a gateway caches an old successful permission response. How do you prevent continued privileged access?

Compare your approach

Authorize under a revocation-aware policy and do not treat a stale cached decision as current authority.

One more constraint

Can a shared cache ever serve authenticated responses safely, and what would have to be enforced?

Quick revision

A shared cache must not answer a personalized request with data created for another identity. A URL-only cache key is unsafe when responses depend on tenant, principal or permissions. For sensitive /api/me/orders use a no-shared-cache policy and edge bypass, with private/no-store directives as appropriate. HTTP shared caches restrict Authorization-bearing responses, but cookie sessions are not automatically safe merely because a CDN has default behavior. Test two tenants and permission revocation through the actual edge. Public assets can remain cacheable.

References

Read across the boundary