afaqrashid.com  ·  portfolio · 2026 ed.

Next.js Environment Variables Undefined: 5 Causes Tested

Most "process.env is undefined" bugs in Next.js come from one fact: variables are read at build time far more often than people expect. I tested it on Next.js 16.4.4 with a SECRET_KEY and a NEXT_PUBLIC_API_URL, read from a client component, a static server page and a dynamic server page. I set them once in .env.local at build time, once only at runtime, and once both with different values.

What each place saw

                            set at build only    set at runtime only    build: "build", runtime: "runtime"
client, NEXT_PUBLIC_        build value          undefined              build value
client, SECRET_KEY          undefined            undefined              undefined
static server page          build values         undefined (both)       build values (both)
dynamic server page:
  SECRET_KEY                -                    runtime value          runtime value
  NEXT_PUBLIC_              -                    runtime value          build value

Every undefined in that table is a cause from the list below. The last row surprised me most: on a dynamic server page, a NEXT_PUBLIC_ variable that existed at build time kept its build value even after I started the server with a different one.

1. No NEXT_PUBLIC_ prefix in a client component

Only variables starting with NEXT_PUBLIC_ reach the browser. SECRET_KEY was undefined in the client component in every run, by design: anything sent to the browser is public. Read secrets in server components, route handlers or server actions, and pass only what the browser needs.

2. NEXT_PUBLIC_ values are frozen at build time

next build replaces process.env.NEXT_PUBLIC_API_URL with the literal string. I found https://build.example written into two of the built JavaScript files under .next/static. Changing the variable afterwards does nothing until you rebuild.

This is the Docker and CI trap. If the image is built without the variable and you pass it with docker run -e or your platform's runtime settings, the client gets undefined: in my run with the variable set only at runtime, the client showed undefined for it. Either provide NEXT_PUBLIC_ variables when the build runs (a build argument in Docker), or don't use NEXT_PUBLIC_ for values that change per environment.

3. Static pages read env once, at build

A server component on a static page is rendered during next build, so even SECRET_KEY came from the build environment. Started with runtime-only variables, the static page printed undefined for both. With different build and runtime values, it printed the build ones.

If a page has to read env at request time, make it dynamic. In Next.js 15 and 16, await connection() does that:

import { connection } from "next/server";

export default async function Page() {
  await connection(); // render per request, so process.env is read at runtime
  return <p>{process.env.SECRET_KEY ? "configured" : "missing"}</p>;
}

That page read SECRET_KEY at runtime in every case.

4. Runtime config for the browser: pass it from the server

To give the browser a value that is only known at runtime, read it in a dynamic server component and pass it as a prop. The dynamic page saw the runtime value whenever the variable hadn't also been set at build time, so name it without NEXT_PUBLIC_ (say API_URL) to keep it from being frozen into the build:

// app/page.tsx (server)
import { connection } from "next/server";
import { Client } from "./Client";

export default async function Page() {
  await connection();
  return <Client apiUrl={process.env.API_URL!} />;
}

5. Dynamic lookups: it depends on the bundler

const name = "NEXT_PUBLIC_API_URL";
process.env[name]; // looked up by a variable, not written out

Next.js's docs say these aren't inlined. In my test the answer depended on the bundler: with Turbopack (the default in Next.js 16) the client got the build value; with next build --webpack it was undefined. Write process.env.NEXT_PUBLIC_API_URL out in full, and don't destructure process.env, so the code works the same with both.

Also check

  • next build prints "- Environments: .env.local" when it loads a file. If it's missing, the file wasn't found: it must be in the project root, not in app/ or src/.
  • .env.local is ignored by most .gitignore files, so CI and your host don't have it. Set the variables in the platform's settings.
  • On Vercel and similar hosts, NEXT_PUBLIC_ variables must be set before the build runs, and a change needs a redeploy.
  • Restart next dev after adding a new variable if it doesn't show up.

Related

Sources