env.dev

Next.js Env Variables Undefined: Causes & Fixes

NEXT_PUBLIC_ vars undefined in the browser, server vars missing, or values frozen after deploy — every Next.js env variable failure mode and its fix.

By env.dev Updated

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 is undefined client-side by design.
  • NEXT_PUBLIC_ values are inlined at next 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 return undefined.
  • next dev only reads .env files at startup — edit one and you must restart the dev server.
  • A stray dotenv dependency stops Next.js loading its own .env files, and an empty value (NEXT_PUBLIC_FOO=) reads as undefined, 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.

SymptomCauseFix
undefined in a Client Component, fine on the serverMissing the public prefixRename to NEXT_PUBLIC_* and rebuild
Public var undefined after changing it in prodValue was frozen at build timeRe-run next build (a redeploy of the same image won't do it)
Var undefined only after you just edited .envnext dev reads .env at startupRestart the dev server
Var undefined via process.env[name]Dynamic key defeats the inlinerUse the full literal expression, no variables/destructuring
Server var undefined in production only.env.local is git-ignored, never deployedSet the var in your host's dashboard / secret store
All NEXT_PUBLIC_ vars undefined, server vars fineA dotenv dependency hijacked loadingRemove 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.

tsx
'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.

bash
# 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 build

If 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:

typescript
// 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:

  1. process.env (the actual shell / CI environment — always wins)
  2. .env.{NODE_ENV}.local (e.g. .env.development.local)
  3. .env.local (skipped when NODE_ENV=test)
  4. .env.{NODE_ENV} (e.g. .env.production)
  5. .env
The /src gotcha: if your app lives in a 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.

tsx
// 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 dotenv is in your package.json, it can pre-empt Next.js's own loader and leave NEXT_PUBLIC_ vars undefined while server vars work. The fix in discussion #12754 was simply npm rm dotenv — Next.js loads .env files itself.
  • An empty value is undefined, not "". Since next@14.2.0, an empty assignment like NEXT_PUBLIC_FOO= evaluates to undefined rather 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.local is 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 .env file to read from in the cloud.

Quick Debugging Checklist

  1. Is the read running in the browser? If so, the var must start with NEXT_PUBLIC_.
  2. Did you reference it as a full literal — no process.env[key], no destructuring, no helper indirection?
  3. For a public var you changed: did you re-run next build (not just redeploy)?
  4. For a dev change: did you restart next dev?
  5. For a server var in prod: is it set in your host's dashboard, not just in a git-ignored .env.local?
  6. Is the file at the project root (not inside src/), and is the value non-empty?
  7. Remove any standalone dotenv dependency.

References

Catch empty values, stray whitespace, and malformed keys before they surface as undefined with the env validator tool.

Was this helpful?

Frequently Asked Questions

Why is my Next.js environment variable undefined?

First check where the code runs. In the browser (Client Components), only NEXT_PUBLIC_-prefixed variables exist; everything else is undefined by design. On the server (Server Components, Route Handlers, proxy/middleware) every variable is available. The other common causes are: a public value changed without re-running next build, the dev server not restarted after editing .env, a dynamic process.env[key] read that the inliner skipped, or the variable simply not being set in your deployment environment.

Why does my NEXT_PUBLIC_ variable still show the old value after I change it?

NEXT_PUBLIC_ variables are inlined into the JavaScript bundle at build time, not read at runtime. After a build the value is frozen, so redeploying the same Docker image or changing the value in your host dashboard has no effect until you re-run next build. If the browser needs a value that varies per environment without a rebuild, expose it from a server Route Handler and fetch it at runtime instead.

Why is process.env[key] undefined in Next.js?

The build-time inliner only replaces the exact literal expression process.env.NEXT_PUBLIC_NAME. Computing the key (process.env[key]), destructuring process.env, aliasing it to another variable, or re-exporting a value through a helper module all defeat the static substitution, so the value never reaches the browser bundle. Reference each public variable directly with its full literal name.

Do I need to restart next dev after editing a .env file?

Yes. next dev reads .env files only once at startup. Hot reload covers component code, not environment variables, so an edited value stays stale until you stop and restart the dev server. This is the most common false alarm — the fix is restarting next dev, not changing code.

Why are my NEXT_PUBLIC_ variables undefined while server variables work?

A standalone dotenv dependency in package.json can pre-empt Next.js's own .env loader and leave NEXT_PUBLIC_ variables undefined while non-prefixed server variables still load. Removing dotenv (npm rm dotenv) fixes it, because Next.js loads .env files itself. Also note that an empty assignment like NEXT_PUBLIC_FOO= has evaluated to undefined rather than an empty string since next 14.2.

Why is my server-only env variable undefined in production but fine locally?

.env.local is git-ignored and never deployed, so it only exists on your machine. In production there is no .env file to read from — you must set server variables in your hosting platform (Vercel, Cloudflare, AWS) or CI secret store. This is correct behavior, not a bug.

Stay up to date

Get notified about new guides, tools, and cheatsheets.