When a Next.js env variable comes back undefined, the cause is almost always one of six things: the variable is missing the NEXT_PUBLIC_ prefix and you are reading it in the browser, you read a server-only var inside a Client Component, you changed a public value but never re-ran next build, you edited a .env file without restarting next dev, you accessed process.env dynamically so the build-time inliner skipped it, or the variable simply is not set in your deployment environment. Each one has a distinct fix, and getting the wrong diagnosis means you rebuild three times and the value is still missing. This page walks every failure mode for the App Router on Next.js 16 (October 2025), where Turbopack is the default bundler and the old publicRuntimeConfig escape hatch no longer exists. For the full mental model of how Next.js loads and exposes env vars, see the Next.js environment variables guide.
TL;DR
- In the browser, only
NEXT_PUBLIC_-prefixed vars exist. Anything else isundefinedclient-side by design. NEXT_PUBLIC_values are inlined atnext build, not read at runtime — change one and you must rebuild, not just redeploy or restart.process.env[key], destructuring, and re-exporting through a helper all defeat the static inliner and returnundefined.next devonly reads.envfiles at startup — edit one and you must restart the dev server.- A stray
dotenvdependency stops Next.js loading its own.envfiles, and an empty value (NEXT_PUBLIC_FOO=) reads asundefined, not"".
Why is my Next.js env variable undefined?
Start by answering one question: where is the code reading the variable running — in the browser or on the server? That single split explains the majority of undefined reports. Non-prefixed variables are only available in the Node.js environment; the official docs are blunt about it: "Non-NEXT_PUBLIC_ environment variables are only available in the Node.js environment, meaning they aren't accessible to the browser." Use this table to map your exact symptom to its cause and fix.
| Symptom | Cause | Fix |
|---|---|---|
undefined in a Client Component, fine on the server | Missing the public prefix | Rename to NEXT_PUBLIC_* and rebuild |
| Public var undefined after changing it in prod | Value was frozen at build time | Re-run next build (a redeploy of the same image won't do it) |
| Var undefined only after you just edited .env | next dev reads .env at startup | Restart the dev server |
Var undefined via process.env[name] | Dynamic key defeats the inliner | Use the full literal expression, no variables/destructuring |
| Server var undefined in production only | .env.local is git-ignored, never deployed | Set the var in your host's dashboard / secret store |
| All NEXT_PUBLIC_ vars undefined, server vars fine | A dotenv dependency hijacked loading | Remove dotenv — Next.js loads .env itself |
Why is NEXT_PUBLIC_ undefined in the browser but not on the server?
Server Components (the App Router default), Route Handlers, and the proxy/middleware layer all run in Node.js, so they can read any variable — process.env.DATABASE_URL works there even with no prefix. Client Components (anything under a 'use client' boundary) ship to the browser, where only the inlined NEXT_PUBLIC_ values exist. Read a server-only var there and you get undefined, every time.
'use client';
export function Banner() {
// undefined — server-only var was stripped from the client bundle
const region = process.env.AWS_REGION;
// works — public var is inlined at build time
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
return <span data-url={appUrl}>{region}</span>;
}A subtle trap: the same component can render on the server during SSR and read the server-only value fine, then hydrate in the browser where it is undefined — producing a hydration mismatch instead of a clean error. If a value flickers or only breaks after hydration, this split is why.
Why does my NEXT_PUBLIC_ change not take effect until I rebuild?
Next.js inlines public vars at build time, replacing every literal process.env.NEXT_PUBLIC_* reference with a hard-coded string. The docs spell out the consequence: "After being built, your app will no longer respond to changes to these environment variables… all NEXT_PUBLIC_ variables will be frozen with the value evaluated at build time." Promote one Docker image from staging to prod, or change the value in your host's dashboard without a rebuild, and the browser keeps the old (or missing) value.
# This does NOT update an already-built NEXT_PUBLIC_ value:
vercel env add NEXT_PUBLIC_API_URL # then redeploy the same build → still stale
# You must trigger a fresh build so the inliner re-runs:
next buildIf the browser needs a value that differs per environment without a rebuild, you cannot use NEXT_PUBLIC_ at all — expose it from a server Route Handler and fetch it at runtime, or read it in a Server Component that has opted into dynamic rendering with connection().
Why is process.env[key] or a destructured var undefined?
The inliner is a literal text substitution — it only matches the exact process.env.NEXT_PUBLIC_NAME expression. Compute the key, destructure the object, or pass process.env around, and there is no literal to replace, so the value never makes it into the bundle. The docs show the exact anti-patterns:
// NOT inlined — key is a variable
const name = 'NEXT_PUBLIC_ANALYTICS_ID';
setup(process.env[name]); // undefined in the browser
// NOT inlined — process.env is aliased
const env = process.env;
setup(env.NEXT_PUBLIC_ANALYTICS_ID); // undefined in the browser
// NOT inlined — destructuring
const { NEXT_PUBLIC_ANALYTICS_ID } = process.env; // undefined
// Inlined — exact literal expression
setup(process.env.NEXT_PUBLIC_ANALYTICS_ID); // "abcdefghijk"This is also why re-exporting public vars through a shared config.ts helper that the browser imports often returns undefined — the read happens behind an indirection the inliner can't see. Reference each public var directly where you use it.
Which .env file is Next.js actually reading?
Next.js looks variables up in a fixed order and stops at the first match, so a value you think is set can be shadowed by a higher-priority file. The order is:
process.env(the actual shell / CI environment — always wins).env.{NODE_ENV}.local(e.g..env.development.local).env.local(skipped whenNODE_ENV=test).env.{NODE_ENV}(e.g..env.production).env
src/ directory, your .env* files must stay at the project root. Next.js loads them "only from the parent folder and not from the /src folder", so a src/.env.local is silently ignored — a classic "the variable is right there but it's undefined" cause.Two more precedence traps: a variable already set in your shell or CI (process.env) overrides every .env file, and in the test environment Next.js loads neither .env.development nor .env.local, so use .env.test. For the underlying .env file syntax (quoting, multiline, $ expansion), see the .env file guide.
Do I need to restart next dev after editing .env?
Yes. next dev reads .env* files once at startup. Hot reload covers your component code, not the environment — edit a value and the running server keeps the old one, so the var looks unchanged or undefined until you stop and restart. This is the single most common false alarm: the fix is Ctrl-C and next dev again, not a code change.
What about the next.config.js env key?
The env key in next.config.js is a separate, build-time-only mechanism — values there are inlined into the bundle like NEXT_PUBLIC_ vars regardless of prefix. The docs warn it has the same inliner constraint: "Trying to destructure process.env variables won't work due to the nature of webpack DefinePlugin." It does not read at runtime, so it is the wrong tool for per-environment values. Next.js 16 also removed publicRuntimeConfig and serverRuntimeConfig entirely, so plain .env files plus the connection() dynamic-render pattern are the only supported channels now.
// app/page.tsx — read a server var at request time, not build time
import { connection } from 'next/server';
export default async function Page() {
await connection(); // prerendering stops here; code below runs per request
const value = process.env.MY_RUNTIME_VALUE; // evaluated at runtime
return <p>{value}</p>;
}When it's not actually a Next.js bug
Most undefined reports are configuration, not framework bugs — but a few real edge cases and known issues are worth ruling out before you spend an afternoon on a misconfiguration that isn't one:
- A stray dotenv dependency. If
dotenvis in yourpackage.json, it can pre-empt Next.js's own loader and leaveNEXT_PUBLIC_vars undefined while server vars work. The fix in discussion #12754 was simplynpm rm dotenv— Next.js loads.envfiles itself. - An empty value is undefined, not "". Since
next@14.2.0, an empty assignment likeNEXT_PUBLIC_FOO=evaluates toundefinedrather than an empty string (issue #64832). If you rely on an empty-string fallback, set an explicit sentinel value instead. - The variable genuinely isn't set. In production,
.env.localis git-ignored and never deployed, so server vars must be configured in your host (Vercel, Cloudflare, AWS). This is correct behavior, not a bug — there is no.envfile to read from in the cloud.
Quick Debugging Checklist
- Is the read running in the browser? If so, the var must start with
NEXT_PUBLIC_. - Did you reference it as a full literal — no
process.env[key], no destructuring, no helper indirection? - For a public var you changed: did you re-run
next build(not just redeploy)? - For a dev change: did you restart
next dev? - For a server var in prod: is it set in your host's dashboard, not just in a git-ignored
.env.local? - Is the file at the project root (not inside
src/), and is the value non-empty? - Remove any standalone
dotenvdependency.
References
- Next.js Docs: Environment Variables — the
NEXT_PUBLIC_prefix, build-time inlining, dynamic-lookup caveat, load order, and the/srcfolder rule. - Next.js Docs: connection() — opting a Server Component into dynamic rendering so a server env var is read per request, not baked in.
- Next.js Docs: next.config.js env — the build-time-only
envkey and why destructuring breaks it. - Next.js 16 release notes — Turbopack as default and the removal of
publicRuntimeConfig/serverRuntimeConfig. - vercel/next.js Discussion #12754 — a stray
dotenvdependency stoppingNEXT_PUBLIC_vars from loading. - vercel/next.js Issue #64832 — empty
NEXT_PUBLIC_values evaluate asundefinedsince 14.2.
Catch empty values, stray whitespace, and malformed keys before they surface as undefined with the env validator tool.