Why might a language-detecting redirect on the site root send every visitor to the same language once a CDN sits in front of it?
answer
- the root is the one request-dependent address
- a cached decision is a frozen decision
- first visitor wins for everyone
- keep detection out of the stored layer
- explicit language must beat detection
basics
~20 sThe redirect is computed from one visitor's request but stored under the URL alone, so the shared cache replays that first decision to everyone. A response chosen from request data must not be cached as if it were the same for all.
solid answer
~50 sThe site root is the one address with no language in it, so the app decides per request — from the stated language preference, a stored choice, or geography — and redirects onward. That redirect is a normal HTTP response, and a shared cache in front of the app will store it under the request URL unless told otherwise. Whoever arrives first freezes the decision: every later visitor gets that same target until the entry expires, which is why a site suddenly serves one language worldwide after a CDN is introduced. The fixes are all about keeping a request-dependent decision out of a request-independent cache: mark the detecting response non-storable by shared caches, or declare the request input the choice depended on so it becomes part of the key; run the decision in a per-request layer that is never cached; or avoid the redirect entirely by serving one cacheable neutral entry page and letting the browser choose where to go.
go deeper
Remember the shape of the bug: a redirect decided from one visitor's request can be stored under the URL and handed to everyone else, so the whole site appears in one language.
Explain why a shared cache keys on the address by default, and what has to be said about a request-dependent response before it is safe to store at all.
Diagnose it end to end — confirm the origin is deciding correctly, find which tier holds the frozen entry, and choose between not storing it, keying it on its input, or moving the decision to a layer that always runs.
Set the policy: where request-dependent decisions may live relative to the cache boundary, how detection interacts with an explicit stored choice, and how that contract is verified in the pipeline rather than discovered in production.
A language-in-the-URL site is almost entirely address-addressable: `/en/pricing` means one thing to everyone. Exactly one address breaks that property — the bare root, which carries no language and therefore has to decide one. That single per-request decision sits in front of an otherwise fully static site, and it is the classic place for a caching bug. ## Why the root is special Every other page is a function of its URL. The root is a function of the *request*: the stated language preference, a previously stored choice, sometimes a geographic hint. Two visitors sending different requests to the same address should get different answers. A shared cache, by default, assumes the opposite — that a URL identifies a response. ## How the decision gets frozen 1. The first visitor requests the root. Their request says they prefer German. 2. The app computes a redirect to the German entry point and returns it. 3. The shared cache stores that redirect under the root URL, because nothing in the response said it depended on the request. 4. Every later visitor — whatever they prefer, wherever they are — is handed the stored redirect without the app ever seeing their request. 5. The bug is invisible in development, invisible behind a cold cache, and appears only once traffic from a second language arrives. It is stored by any shared cache that treats the response as cacheable, which many redirects are by default, and a status that signals permanence makes it worse: clients and intermediaries may hold it far longer than intended, so the wrong language sticks even after the origin is fixed. ## What the symptom looks like | Report | What is really happening | |---|---| | Everyone lands in one language | one stored decision replayed to all | | It is right for the team, wrong for users | the team primed the cache first | | It was fine yesterday, wrong today | a new cache tier, or an entry re-populated by a different first visitor | | Clearing cookies does not help | the decision never reaches the app at all | | Fixed at the origin, still wrong | a long-lived entry stored downstream, or held by the browser | ## Designs that keep detection out of the cached layer - **Do not store it.** Mark the detecting response as not storable by shared caches, accepting that the root is then an origin hit for every visitor. It is one small response, and the language pages behind it stay fully cacheable. - **Make the input part of the key.** Declare the request header the decision depended on, so the cache stores one entry per distinct value. This works but fragments the entry across the long tail of header values; how to design that key belongs to HTTP caching. - **Decide in a layer that always runs.** A per-request hook that executes before route resolution on every request, by construction not served from a stored copy, can do the detection while everything behind it stays static. - **Do not redirect at all.** Serve one cacheable, language-neutral entry page and let the browser decide where to go after load, or present an explicit choice. The cached artifact is then the same for everybody, and the per-visitor part happens where per-visitor decisions are cheap. ## Rules that keep detection sane - **Never redirect a URL that already names a language.** Detection belongs only where the language is absent; applying it everywhere overrides explicit choices and invites redirect loops between two language URLs. - **An explicit choice must win.** Once someone picks a language, record it and honour it; a detector that re-guesses on every visit is experienced as the site fighting the user. - **Detect once, then leave them alone.** A redirect that fires on every entry makes deep links unreliable and breaks back-navigation, since the visitor is bounced forward again. - **Remember the clients with no preference.** Crawlers and scripted clients often state none. They should reach a defined default, not an empty decision — and if the root is the only entry point they know, they may only ever see that one language. - **Verify with a second language.** The test that catches this is not one request; it is two requests with different stated preferences through the real cache, checking that each is answered independently.
- Why does the same bug not appear in local development?Locally there is usually no shared cache between the browser and the app, so every request reaches the detection code and every visitor is answered individually. The bug is a property of the deployment topology, not of the code, which is why it should be tested through the real caching layer with two different stated preferences rather than on a developer machine.
- What goes wrong if the detection redirect is applied to every URL, not just the root?Explicit choices stop working: a visitor who deliberately opens a language URL is bounced to the detected one, deep links become unreliable, and back-navigation re-triggers the redirect. Two rules that disagree can also bounce a request between languages until the browser gives up. Detection belongs only where the address carries no language.
- If the root must stay cacheable, what can still be personalised?Serve one neutral, fully cacheable entry document to everyone and do the per-visitor part after it arrives — read the preference in the browser and navigate onward, or simply show a language chooser. The cached artifact is identical for all visitors, and the decision happens where per-visitor state is cheap and no shared cache can freeze it.
A receptionist who asks the first guest of the morning which language they speak, writes it on a sign, and then silently points every later guest down that same corridor without asking again.
saying these in an interview costs you the question
- Thinks shared caches never store redirect responses
- Uses a permanent redirect for a per-visitor decision
- Applies language detection to URLs that already name a language
- Tests detection with a single request and calls it verified
- Assumes clearing the browser's cookies proves the origin is wrong
- Re-guesses the language on every visit, overriding an explicit choice