Why Your Next.js Env Variables Aren't Working in 9.4.4 and How to Configure Them

Next.js 9.4.4 requires environment variables to be defined in .env files at the project root and uses the NEXT_PUBLIC_ prefix to expose values to the browser, with variables being inlined at build time rather than read at runtime.

Next.js 9.4 introduced built-in support for .env files through the @next/env package, but misconfigurations in file placement or naming conventions often cause nextjs env variables to fail silently. This guide examines the actual implementation in the vercel/next.js repository to explain why variables disappear and how to properly configure them in version 9.4.4.

Common Reasons Next.js 9.4.4 Env Variables Fail

Missing or Misplaced .env Files

The most common cause of undefined values is placing .env files outside the project root or using incorrect file names. According to the source code in packages/next-env/index.ts, Next.js only looks for files at the repository root and follows a specific loading order based on the current mode.

Missing NEXT_PUBLIC_ Prefix for Client Access

Variables without the NEXT_PUBLIC_ prefix are server-only by design. If you try to access process.env.API_URL in a browser component, it returns undefined because the Next.js bundler only inlines variables starting with NEXT_PUBLIC_ into the client bundle.

Build-Time vs Runtime Confusion

Environment variables are statically inlined at build time for the client bundle. Changing a variable in your hosting dashboard after running next build has no effect on the client-side code. This surprises many developers who expect runtime configuration changes to reflect immediately.

How Next.js Loads Environment Variables (Source Analysis)

The environment loading logic resides in packages/next-env/index.ts and centers around the loadEnvConfig function. When the Next.js server starts—whether in development, build, or production mode—it executes the following sequence:

  1. Determines the mode based on NODE_ENV and the dev flag (development, test, or production)
  2. Builds the file list in priority order:
    • .env.{mode}.local
    • .env.local (skipped in test mode)
    • .env.{mode}
    • .env
  3. Parses each file using dotenv and dotenv-expand to handle variable expansion
  4. Merges values into process.env (only once per process unless forceReload is true)

The NEXT_PUBLIC_ filtering happens later during the webpack build phase. The bundler searches for process.env.NEXT_PUBLIC_* references and replaces them with literal string values, ensuring no server secrets leak to the browser.

Configuring Server-Only and Public Variables

Server-Only Variables (No Prefix)

Use these for database credentials, API secrets, or any value that should never reach the client. Define them in your .env file without any special prefix:


# .env.development

DATABASE_URL=postgres://user:password@localhost:5432/mydb
SECRET_API_KEY=sk_live_1234567890

Access them only in server contexts like API routes or getServerSideProps:

// pages/api/data.ts
export default function handler(req, res) {
  // ✅ Safe to use here - never exposed to browser
  const dbUrl = process.env.DATABASE_URL;
  res.json({ status: 'connected', db: dbUrl?.split('@')[1] });
}

Public Variables (NEXT_PUBLIC_ Prefix)

Prefix any variable needed in the browser with NEXT_PUBLIC_:


# .env.production

NEXT_PUBLIC_ANALYTICS_ID=UA-123456-1
NEXT_PUBLIC_API_URL=https://api.example.com

Reference them in any component:

// components/Analytics.tsx
export default function Analytics() {
  // ✅ Inlined at build time, available in browser
  const trackingId = process.env.NEXT_PUBLIC_ANALYTICS_ID;
  
  return (
    <script
      async
      src={`https://www.google-analytics.com/analytics.js?id=${trackingId}`}
    />
  );
}

Advanced Configuration Patterns

Using Environment Variables in next.config.js

In Next.js 9.4+, next.config.js executes after Next.js automatically loads your .env files. You can safely reference process.env variables to customize the build:

// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Access env vars that were loaded from .env files
  env: {
    CUSTOM_BUILD_ID: process.env.BUILD_ID || 'dev',
  },
  // Use server-only vars for redirects/rewrites
  async redirects() {
    return [
      {
        source: '/old-api',
        destination: process.env.NEW_API_ENDPOINT || '/api',
        permanent: true,
      },
    ];
  },
};

export default nextConfig;

Runtime Configuration via API Routes

When values change after build or need to be dynamic, expose them through an API route instead of relying on client-side env vars:

// pages/api/config.ts
export default function handler(req, res) {
  // Reads current env at request time, not build time
  res.json({
    apiUrl: process.env.NEXT_PUBLIC_API_URL,
    featureFlag: process.env.ENABLE_BETA_FEATURES === 'true',
  });
}
// components/RuntimeConfig.tsx
import useSWR from 'swr';

export default function RuntimeConfig() {
  const { data, error } = useSWR('/api/config', (url) => 
    fetch(url).then((r) => r.json())
  );
  
  if (error) return <div>Failed to load config</div>;
  if (!data) return <div>Loading...</div>;
  
  return <div>API URL: {data.apiUrl}</div>;
}

Debugging Environment Variable Loading

If variables remain undefined, verify which files Next.js actually loaded using the internal debug command:


# Shows loaded env files and parsed values

node -e "require('@next/env').loadEnvConfig(process.cwd(), true)"

This outputs the loadedEnvFiles array containing the path and parsed env map for each file, confirming whether .env.development or .env.local was actually read by the loader in packages/next-env/index.ts.

Summary

  • Place .env files at the project root: Next.js 9.4.4 only loads files from the repository root following the specific order: .env.{mode}.local, .env.local, .env.{mode}, then .env.
  • Use NEXT_PUBLIC_ for browser access: Variables without this prefix remain server-only; the bundler inlines only NEXT_PUBLIC_* variables into the client bundle.
  • Remember build-time inlining: Client-side environment variables are frozen at next build; changes after building require a rebuild or runtime API exposure.
  • Debug with @next/env: Use loadEnvConfig programmatically to verify which files loaded and what values were parsed.

Frequently Asked Questions

Why is process.env.MY_VAR undefined in my Next.js 9.4.4 API route?

This typically occurs because the .env file is not located at the project root or the file name doesn't match the current NODE_ENV mode. Next.js looks for .env.development when running next dev and .env.production when running next start after a build. Ensure your file is named correctly and placed at the repository root, not in subdirectories like src/.

Why does my environment variable work on the server but return empty on the client?

Next.js 9.4.4 treats environment variables as server-only by default to prevent accidental exposure of secrets. The bundler only inlines variables prefixed with NEXT_PUBLIC_ into the browser JavaScript bundle. If you reference process.env.API_KEY without the prefix in a React component, it evaluates to an empty string because the variable was never sent to the client.

Can I change environment variables after building my Next.js application?

No, client-side environment variables are statically inlined during next build and cannot change without rebuilding. If you need dynamic configuration that changes per deployment or at runtime, expose the values through an API route (e.g., /api/config) and fetch them when your application mounts. This approach reads process.env at request time rather than build time.

How do I verify which .env files Next.js 9.4.4 is actually loading?

You can programmatically inspect the loaded configuration by running the internal loader function. Execute node -e "require('@next/env').loadEnvConfig(process.cwd(), true)" in your terminal. This outputs the loadedEnvFiles array showing exactly which files were parsed and their resolved values, helping you identify if .env.local or .env.development is being skipped.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →