Meta Description: Environment variables not loading in Next.js? I break down every common cause — from NEXT_PUBLIC_ prefixes to restart issues — with real fixes that worked for me.
Why Your Environment Variables Aren’t Loading in Next.js (And How I Fixed It Every Time)
The first time this happened to me, I spent almost an hour convinced I’d misspelled a variable name. I hadn’t. I’d just misunderstood how Next.js actually handles environment variables — and once I understood the rules, the bug stopped being a mystery and started being a five-minute fix.
If process.env.MY_VARIABLE is coming back undefined, or your .env.local file seems to be completely ignored, you’re not alone. This is one of the most common issues developers run into with Next.js, and in my experience, it almost always comes down to one of a handful of specific causes. I’m going to walk you through every one of them, in the order I’d actually check them if this happened to me today.
Quick Answer: The Most Common Reason This Happens
If you only read one paragraph, read this one. Environment variables usually “don’t load” in Next.js because of one of three things: the file isn’t named or placed correctly, the dev server wasn’t restarted after the variable was added, or the variable is being accessed on the client side without the NEXT_PUBLIC_ prefix. In my experience, that third one accounts for more support tickets and Stack Overflow posts than everything else combined.
Now let’s go through each cause properly, because “restart the server” isn’t always the answer, and I don’t want to send you away with a half-fix.
How Next.js Actually Loads Environment Variables
Before troubleshooting, it helps to know what Next.js is doing under the hood. Unlike a plain Node.js app where you’d typically reach for the dotenv package yourself, Next.js has built-in support for .env files. It loads them automatically at build time and at server start, using a specific priority order.
Next.js looks for these files, in this order of precedence (highest priority first):
.env.$(NODE_ENV).local.env.local(not loaded whenNODE_ENVistest).env.$(NODE_ENV).env
Variables defined in a higher-priority file override the same variable defined in a lower-priority one. I’ve seen developers lose 20 minutes because they had the same key defined in both .env and .env.local with different values — and .env.local was silently winning.
Where This Trips People Up
The most important thing to internalize is that there are two completely separate environments where your code runs: the server (Node.js) and the browser (client). Environment variables behave differently in each, and that split is where almost every “not loading” bug originates.
Cause #1: Missing the NEXT_PUBLIC_ Prefix (Client-Side Access)
This is, by far, the number one cause I run into — both in my own projects and when helping other developers debug theirs.
By default, environment variables in Next.js are only available on the server. This is a deliberate security decision: you don’t want your API secrets, database credentials, or private tokens accidentally bundled into the JavaScript that ships to the browser.
If you try to access a plain environment variable from client-side code — a React component, a hook, anything that runs in the browser — it will be undefined.
// .env.local
API_SECRET_KEY=super-secret-value
// components/MyComponent.jsx (client component)
'use client'
export default function MyComponent() {
console.log(process.env.API_SECRET_KEY) // undefined in the browser
return <div>Hello</div>
}
To expose a variable to the browser, you have to prefix it with NEXT_PUBLIC_:
# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
API_SECRET_KEY=super-secret-value
'use client'
export default function MyComponent() {
console.log(process.env.NEXT_PUBLIC_API_URL) // works fine
console.log(process.env.API_SECRET_KEY) // still undefined — and that's correct
return <div>Hello</div>
}
Here’s what worked for me as a mental model: if a value needs to reach the browser (a public API URL, a Stripe publishable key, an analytics ID), prefix it. If it’s a secret that should only ever touch your server (database URLs, private API keys, JWT secrets), never prefix it — and never try to.
Pro Tip: Don’t just add
NEXT_PUBLIC_to everything “to be safe.” Every variable with that prefix gets inlined into your client JavaScript bundle at build time, in plain text. Anyone can open dev tools and read it. I treat the prefix as a one-way door — once it’s public, assume it’s public forever, even if you remove the prefix later and forget to rotate the secret.
Cause #2: You Didn’t Restart the Dev Server
This one feels almost too simple to mention, but it’s genuinely the second most common cause I see.
Next.js reads your .env files when the server process starts — not on every request, and not via hot reload. If you add a new variable, rename one, or change a value while next dev is already running, that change will not be picked up until you stop and restart the server.
# Kill the running dev server (Ctrl+C), then:
npm run dev
I learned this the hard way early on, when I added a new API key, watched it come back undefined, and spent way too long assuming my .env.local syntax was broken. It wasn’t — the running process just hadn’t reloaded the file.
Fast Refresh handles your component code beautifully, but environment variables aren’t part of that hot-reloading system. Treat any .env file change as something that requires a full restart, every time.
Cause #3: Wrong File Name or Wrong Location
Next.js is strict about file naming. A few mistakes I’ve seen repeatedly:
- Naming the file
.env.local.txt(some editors add this extension silently on Windows) - Placing the
.env.localfile inside a subfolder instead of the project root - Using
env.localinstead of.env.local(missing the leading dot) - Typos like
.env.locolor.envlocal
The file must be named exactly .env.local (or one of the other valid variants listed earlier) and must live in the root of your Next.js project, next to package.json and next.config.js.
my-next-app/
├── .env.local ✅ correct location
├── next.config.js
├── package.json
├── app/
│ └── .env.local ❌ this will be ignored
If you’re on macOS or Linux, run ls -la in your project root to confirm the file actually exists and is named correctly — hidden files won’t show up with a plain ls.
Cause #4: Confusing Server Components, Client Components, and API Routes
With the App Router, this gets a little more nuanced than it was in the Pages Router, because you now have Server Components, Client Components, Route Handlers, and Middleware — and they don’t all treat environment variables the same way.
| Context | Can access non-prefixed vars? | Can access NEXT_PUBLIC_ vars? |
|---|---|---|
| Server Component | ✅ Yes | ✅ Yes |
| Client Component | ❌ No | ✅ Yes |
Route Handler (route.ts) | ✅ Yes | ✅ Yes |
| Middleware | ✅ Yes (limited runtime) | ✅ Yes |
getStaticProps / getServerSideProps (Pages Router) | ✅ Yes | ✅ Yes |
In my experience, the bug usually shows up when a developer refactors a component from a Server Component into a Client Component (by adding 'use client') without realizing that the environment variable access inside it now needs the NEXT_PUBLIC_ prefix. The component worked yesterday; today it’s silently broken, and nothing in the error output tells you why.
Cause #5: Variables Set at Build Time Aren’t Updating
This one catches people deploying to platforms like Vercel, Netlify, or a custom Docker pipeline.
NEXT_PUBLIC_ variables are inlined into the JavaScript bundle at build time, not read at runtime. If you change an environment variable in your hosting dashboard but don’t trigger a new build, the old value stays baked into your already-built static assets.
After deploying this in production more than once, this is the checklist I run through:
- Confirm the variable is set correctly in your hosting provider’s dashboard (not just locally)
- Confirm it’s set for the correct environment (Production vs. Preview vs. Development — these are often separate on Vercel)
- Trigger a new deployment, not just a redeploy of the same build artifact
- If using Docker, confirm the variable is passed at
docker buildtime (via--build-arg) if it needs to be inlined, or atdocker runtime if it’s server-only
# Dockerfile — passing a NEXT_PUBLIC_ variable at build time
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
docker build --build-arg NEXT_PUBLIC_API_URL=https://api.example.com -t my-app .
Server-only variables, on the other hand, can be injected at runtime, which is actually more flexible — you can change them without rebuilding the image.
Cause #6: Environment Variables in next.config.js
There’s a separate, older mechanism where you can expose variables through the env key in next.config.js. It still works, but I’d steer you away from it for new projects — it’s mostly legacy from before NEXT_PUBLIC_ existed, and it applies to every build regardless of environment, which can cause confusing overrides.
// next.config.js — legacy approach, avoid unless you have a specific reason
module.exports = {
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},
}
If you inherited a project using this pattern and env variables aren’t behaving the way you expect, check next.config.js first — a value hardcoded or misconfigured here will override what’s in your .env.local file, and it’s easy to forget it’s there.
Debugging Checklist
When I hit this issue now, this is the exact order I check things in:
- [ ] Is the file named exactly
.env.local(or the correct variant) with no typos? - [ ] Is the file in the project root, not a subfolder?
- [ ] Did I restart the dev server after adding or changing the variable?
- [ ] Am I accessing this variable from client-side code? If so, does it have the
NEXT_PUBLIC_prefix? - [ ] Is there a conflicting value in
next.config.js‘senvkey? - [ ] In production, is the variable set correctly in the hosting dashboard for the right environment?
- [ ] Did I trigger a fresh build after changing a
NEXT_PUBLIC_variable in production? - [ ] Am I logging
process.env.MY_VARfrom the right context (server log vs. browser console)?
Common Mistakes I See Developers Make
Committing .env.local to Git. Next.js’s default .gitignore (generated by create-next-app) already excludes .env*.local, but I’ve seen developers remove that line accidentally while cleaning up their .gitignore, then wonder why their secrets are on GitHub. Always double-check.
Assuming process.env works the same as in plain Node.js. In plain Node, every variable is a string available everywhere at runtime. In Next.js, the client/server split and the build-time inlining behavior are fundamentally different, and treating them the same causes exactly the bugs in this article.
Not knowing the difference between .env and .env.local. .env is meant for defaults that are safe to commit (non-secret config, shared across the team). .env.local is for your personal overrides and secrets, and is gitignored by default. Mixing these up leads to either committed secrets or missing shared defaults.
Forgetting that TypeScript won’t warn you. process.env.MY_VAR is typed as string | undefined by default, and unless you’ve set up a validation layer (like zod or @t3-oss/env-nextjs), TypeScript won’t catch a missing variable — you’ll only find out at runtime.
A Better Long-Term Fix: Validate Your Environment Variables
After running into this enough times across different projects, I stopped just fixing it reactively and started validating environment variables at startup instead. Libraries like @t3-oss/env-nextjs or a simple zod schema let your app fail loudly and immediately if a required variable is missing, instead of failing silently three components deep.
// env.ts
import { z } from 'zod'
const envSchema = z.object({
NEXT_PUBLIC_API_URL: z.string().url(),
API_SECRET_KEY: z.string().min(1),
})
export const env = envSchema.parse({
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
API_SECRET_KEY: process.env.API_SECRET_KEY,
})
If a required variable is missing, this throws immediately at build/start time with a clear error message, instead of letting undefined silently propagate through your app until something breaks in a confusing way three layers deep.
FAQ
Why is process.env.MY_VARIABLE undefined in my Next.js app? Most commonly, it’s because you’re accessing the variable from client-side code without the NEXT_PUBLIC_ prefix, or you added the variable to .env.local but never restarted the dev server. Check both first.
Do I need to restart Next.js after changing .env.local? Yes. Next.js reads environment variable files when the server process starts, not on every request or via hot reload. Any change to a .env file requires a full restart of next dev or a new build in production.
What’s the difference between .env and .env.local in Next.js? .env is intended for default values that are safe to commit to version control and share across your team. .env.local is for local overrides and secrets, and Next.js’s default .gitignore excludes it from Git automatically.
Why do I need NEXT_PUBLIC_ for some variables but not others? Next.js only exposes variables to browser-side JavaScript if they’re prefixed with NEXT_PUBLIC_. This is a security measure so that secrets in your .env files aren’t accidentally bundled into client-facing code.
Why did my environment variables stop working after deploying to Vercel? Usually because the variable wasn’t set in the Vercel dashboard for the correct environment (Production, Preview, or Development are separate), or because a NEXT_PUBLIC_ variable was changed without triggering a new deployment — these values are inlined at build time.
Can I use a .env file with the Next.js Edge Runtime or Middleware? Yes, but the Edge Runtime has a more limited Node.js API surface. Standard environment variables work in Middleware, but be aware that some Node-specific env-related packages (like dotenv itself) may not work in the Edge Runtime — Next.js’s built-in env loading handles this natively, so you generally don’t need dotenv at all.
Final Thoughts
Almost every “environment variables not loading” issue in Next.js comes down to one of the causes above, and in my experience, the NEXT_PUBLIC_ prefix and the “forgot to restart the server” issue account for the vast majority of cases. Once you understand that Next.js treats server and client code as genuinely separate environments — and that build-time inlining is different from runtime reads — this stops being a mystery and becomes a quick checklist to run through.
If this helped you sort out your environment variables, take a look at some of the other Next.js and web development guides here on SpiritCode.blog — I write these based on real issues I’ve hit building and shipping production apps, not just theory.

