Why does changing an environment variable on a running server not change a value already inlined into the client bundle?
answer
- frozen at build, not read later
- a literal, not a lookup
- restart changes the server side only
- the artifact is built for one environment
- grep the emitted chunk for the value
basics
~20 sBecause the build replaced that read with a string literal. The emitted JavaScript contains no lookup to re-run, so restarting the server or editing its environment changes nothing. Only a new build, or a value delivered at render, moves it.
solid answer
~40 sFor client-bound code, reading an environment variable is a **build-time** operation, not a runtime one. The bundler finds each read of a publicly marked name and rewrites it into a literal string, after which the optimiser may fold branches around that constant. What ships is an ordinary static file: it has no expression left that could consult an environment, and the browser has no environment to consult anyway. So editing the variable on the server, restarting the process, or redeploying the *same* artifact all leave the shipped value exactly as it was. The two real levers are producing a new build with the new value, or moving that value off the inlined path entirely so the server reads it per request and passes it to the client at render time.
go deeper
Hold on to the timing: for browser code the value is decided when the build runs, so a change in the deployment's settings shows up only after the next build.
Explain the mechanism precisely — the read is replaced by a string literal, the optimiser folds around it, and the shipped file contains no expression that could look anything up.
Show the diagnosis: search the emitted chunks for the stale literal, compare what the build step saw against what the runtime step has, and validate required public names before the pipeline finishes.
Reason about the consequence for release: an inlined value makes the artifact environment-specific. Decide deliberately which values may be frozen and which must be delivered at render, and make that boundary explicit.
## Substitution, not lookup When a meta-framework prepares code for the browser, a read of a publicly marked environment variable is not compiled into "go and look this up later". It is **textually replaced** with the value the build machine had at that moment. A comparison like `flag === 'on'` becomes a comparison between two literals, and the optimiser is then free to decide the branch and delete the loser. The output is a static asset. Nothing in it is capable of asking a question at runtime, so there is no mechanism by which a later change could reach it. ## What the build freezes, step by step 1. The build process starts and inherits an environment from whatever launched it — a pipeline runner, a container, a developer's shell. 2. Client-bound modules are scanned; each read of a marked name is rewritten to a literal. 3. The optimiser folds constants, eliminates branches now provably dead, and minifies. 4. The result is hashed into filenames and written to the output directory. 5. Those exact bytes are uploaded, usually cached by a CDN, and cached again by browsers under a long-lived policy because the filename is content-hashed. Every step after (2) is operating on a value that has already stopped being a variable. ## Why the usual remedies do nothing | Action | Effect on an inlined client value | |---|---| | Edit the variable in the deployment's settings | None — the artifact is already written | | Restart or scale the server process | None — the new process re-reads the environment for *server* code only | | Redeploy the same build artifact | None — the same bytes are served again | | Purge the CDN cache | None — the origin still holds the same file | | Run a new build with the new value | The value changes, and filenames change with it | | Read the value on the server per request and pass it to the client at render | The value changes without a build, at the cost of carrying it in the response | The first four are where teams lose an afternoon, because every one of them is the correct remedy for a *server-side* configuration change and none of them touches a client one. ## The same variable, two different lifetimes A single name can be read on both sides of the seam, and it behaves differently on each: - **Server read** — evaluated when that code runs, so it reflects the environment of the currently running process. Change it and redeploy the process, and the next request sees the new value. - **Client read** — evaluated once, by the build, and shipped as a constant. Change it and you must rebuild. That difference is what makes "it works on the server but the browser shows the old value" such a common report. Both reads look identical in source. ## Diagnosing it - **Search the emitted files for the literal.** If the old value is sitting in a shipped chunk, the question is settled: it was inlined, and only a rebuild moves it. This is also the cheapest audit for values that should never have been there. - **Check what the build machine saw**, not what the server has. Pipelines routinely expose different variables to a build step and to a runtime step, and a value absent at build becomes an empty literal rather than an error. - **Watch for a value that changed only after the next unrelated deploy.** That is the signature of a rebuild picking it up, and it confirms the inlining path. ## What it means for environment promotion Because the value is frozen at build time, a bundle is implicitly built *for one environment*. Promoting the identical artifact from staging to production carries the staging values with it — which is why teams either build once per environment or deliberately keep environment-varying values off the inlined path and deliver them at render instead. Values that genuinely do not vary — a public identifier, a build stamp, a flag flipped only by release — are the ones that inline comfortably. ## A related trap: the value that was never there If the variable was undefined on the build machine, the substitution still happens; it just produces an empty literal. Nothing warns you at request time, because at request time there is nothing left to warn about. Validating required public names at the end of the build — assert they are non-empty, fail the build if not — converts a silent production defect into a pipeline failure.
- The same variable is read in both server-side and client-side code. Why can the two sides disagree after a configuration change?Because they are evaluated at different times. The server read happens when the request is handled, so it reflects the current process environment. The client read was resolved by the build and shipped as a constant. Change the deployment's value and the server updates immediately while the browser keeps the build-time value until the next build.
- A required public variable was missing on the build machine. What does client code see, and how would you catch it earlier?It sees an empty value — the substitution still occurs, it just inserts nothing — so the failure surfaces as a broken feature in production rather than an error. Catch it by validating required public names as a build step and failing the pipeline when one is absent or empty, the same way you would validate server configuration at startup.
- Does dead-code elimination around an inlined flag make the value impossible to recover from the bundle?No. Folding removes the branch the constant disproved, but the constant itself is substituted at every read, and only provably unreachable code is dropped. Any value used in a live path is present verbatim. Treat elimination as an optimisation, never as concealment.
It is the difference between a printed menu and asking the kitchen. A price printed in the morning stays printed all day no matter what the kitchen decides at noon; you have to reprint. Server-side reads ask the kitchen each time.
saying these in an interview costs you the question
- Says restarting the server refreshes values in the browser bundle
- Believes the bundle re-reads configuration on each page load
- Blames CDN caching for a value that was inlined at build
- Thinks one artifact can be promoted unchanged across environments regardless
- Assumes a missing build-time variable raises an error at runtime
- Treats constant folding as a way to hide an inlined value