skip to content

In a meta-framework, how does a cached copy kept on the server differ in scope from one kept in the browser tab?

level: juniorimportance: must knowfreq 62%

answer

  1. ask who can be served it
  2. two axes: lifetime and scope
  3. server-side is not per-user
  4. the key defines the audience
  5. browser copy dies on full load

basics

~20 s

A copy kept on the server is shared: any request matching its key can be served it, including another visitor's. A copy in the browser tab is private to that browser and gone on a full page load.

solid answer

~50 s

Cached copies differ on two axes: how long they live, and who can be handed them. Within one render, a dedupe cache holds a read only until the response is finished, so it is private to that request. A store on the server outlives the request, so a copy placed there can be handed to any later request whose key matches, including a different, signed-in visitor. The rendered output of a route held for reuse is shared the same way. The payload the browser keeps for routes it has already visited is private to that tab and disappears on a full page load. The practical rule: a read derived from who is asking belongs in the request-scoped or the browser-side copy, never in a copy that outlives the request unless every identity input is part of the key.

go deeper

for a junior

Remember the two questions to ask of any cache: how long does the copy live, and who can be served it. Server-side means shared; in the browser tab means private to that browser.

for a middle

Be able to walk the placements from shortest-lived to longest and say what discards each one, and explain why the cache key is what actually decides a shared copy's audience.

for a senior

Show that you decide placement when you write the read, and that you can point at which placement a suspect response came from using age or debug headers, a latency cliff, and a second identity.

for a principal

Frame it as a blast-radius decision: a wrong lifetime costs staleness, a wrong scope costs a data-exposure incident, and the two deserve different review and testing rigour.

## Why placement is the real question Saying a value is "cached" says almost nothing useful. Two properties decide whether a cache is a performance win or a security incident: - **lifetime** - when the copy is discarded; - **scope** - who can be served it. A meta-framework typically offers several places to put a copy, and those places differ on both axes. Choosing where a read's result lives is choosing its audience, and that choice is made at the moment you mark a read as cacheable - not later, when someone reads the output. ## The places a copy can live | Placement | Lives in | Discarded when | Who can be served it | | --- | --- | --- | --- | | Per-render dedupe | the work of one request | that response is finished | only that request | | In-process store | one server instance's memory | the process restarts, or on eviction | every visitor that instance serves | | Stored render output | storage the server reads from | expiry, removal, or a new build | every visitor whose request matches the key | | Client route payload | the browser tab's memory | a full page load, or the tab closes | that one browser | Several consequences fall straight out of that table: - Only the first and last rows are **naturally private**. The middle two are **shared by construction**: putting a copy "on the server" makes it less private, not more. - A copy that outlives the request also outlives the **identity** that produced it. Storage keeps bytes and a key; it keeps no memory of who was signed in when those bytes were made. - Lifetime and scope are independent. A browser's copy can easily outlive an in-process copy - a process restart wipes one and not the other - and it is still the more private of the two. - The **key** is what defines the audience of a shared copy. Two requests that compute the same key are, as far as the cache is concerned, the same request. ## The rule that follows A read is *personalised* when its result depends on who is asking. Signals that you are looking at one: - it is derived from a session cookie, an authorization header, or any identity token; - it varies by role, entitlement, plan, or tenant; - it contains anything you would not be willing to print on a fully public page. A personalised read is safe in a request-scoped copy (it dies with the response) and in the browser's own copy (it never leaves that browser). It is unsafe in a copy that outlives the request **unless every input that varies the output is part of the key** - and per-user keys carry their own costs in entry count and hit rate. ## What a leak actually looks like The failure mode is mechanical, not exotic: 1. A request from a signed-in visitor produces a render that includes their name, balance, or order list. 2. That render is stored under a key computed from the URL - identity was never an input. 3. A later request from anyone else resolves to that same key and is served the stored bytes, name and all. The symptom is intermittent and correlated with traffic: it only happens when a second request arrives while the copy is still around. It usually cannot be reproduced in a development server, because the placements that store output are often inactive there. And a hard refresh sometimes appears to "fix" it, which sends people hunting in the browser instead of in the store. ## Frameworks do not all ship the same set Meta-frameworks differ in how many of these placements they actually provide. Some ship all of them with caching applied by default, so the interesting work is opting particular reads out. Others ship only the client-side payload and leave everything shared to standard HTTP caching and to whatever the host puts in front of the origin. Because of that, memorising one product's tier list transfers badly. What transfers is the habit of asking, for any given read: *how long does this copy live, and who can be handed it?* ## Telling copies apart in practice When you need to know which copy answered a request, useful signals include: - an **age** or debug header on the response, where the framework or host emits one; - a **latency cliff** - a stored copy answers far faster than a render that hits a database; - changing the underlying data and seeing the response not change; - comparing a response in a fresh private window against one in the original browser, which separates a browser-side copy from a server-side one. None of that is needed to make the right decision up front. Placement is decided when the read is written, and the question to answer then is simply whether a stranger being handed this exact response would be acceptable.

  • Does signing out remove a personalised copy that was already stored on the server?
    No. Signing out changes what the browser sends on the next request; it does not reach into a store on the server. A copy produced during a signed-in render stays there until it expires or is explicitly removed, and any later request whose key matches can still be served it. That is the reason a personalised render should not be placed in a shared copy in the first place.
  • If two server instances each hold their own in-process copy, what does that change about scope?
    Scope becomes per instance: the same URL can be answered from a different copy depending on which instance takes the request, so behaviour looks inconsistent between refreshes and a problem may reproduce on one instance and not another. Anything you do to a copy in memory affects only the instance you did it on.

A copy on a shared server is like a printout left in the office printer tray: whoever walks up next can pick it up. A copy in your browser tab is the note in your own pocket.

saying these in an interview costs you the question

  • Thinks a cache on the server is private because it is server-side
  • Assumes signing out removes copies already stored on the server
  • Treats cached as one thing with one lifetime and one audience
  • Believes the browser's route payload is visible to other visitors
  • Cannot say who would be served a given stored copy