Skip to main content

How to Read Environment Variables at Runtime in Next.js

· 7 min read
Jagdish Kumawat
Founder @ Dewiride

A Next.js page that reads process.env can be built ahead of time — and then it shows the value from the build forever, whatever you set on the server later. await connection() tells Next.js to wait for a real request before reading it. This guide shows both, with one build started with a different value.

Why a Page Can Keep the Build's Value​

next build prepares as much of every page as it can in advance. A page that doesn't depend on the request — no cookies, no headers, no connection() — is prerendered: Next.js runs it once during the build and stores the HTML. Reading process.env doesn't count as depending on the request, so the value at build time ends up in the stored HTML.

That is fine for settings that are fixed at build time. It breaks the moment you want one build that runs in several places — a test and a production environment, or one chat app for several agents — each with its own values. Next.js's guide describes the alternative: You can safely read environment variables on the server during dynamic rendering, and the way to make rendering dynamic is to wait for the request.

Prerequisites​

  • A Next.js app with the App Router. The examples use Next.js 16.4.6 with Cache Components on, as create-next-app sets it up.
  • A terminal in the app's folder.

Step 1: A Page Without connection()​

This page reads a setting called GREETING:

app/at-build/page.tsx
// Reads the setting without waiting for a request.
export default function AtBuild() {
return (
<main style={{ padding: 48 }}>
<h1>Without connection()</h1>
<p>GREETING is: {process.env.GREETING}</p>
</main>
);
}

Step 2: A Page With connection()​

The same idea, with the reading moved into a component that waits for the request:

app/page.tsx
import { connection } from "next/server";
import { Suspense } from "react";

export default function Home() {
return (
<main style={{ padding: 48 }}>
<h1>With connection()</h1>
<Suspense>
<Greeting />
</Suspense>
</main>
);
}

// Waits for a real request, then reads the setting from the running server.
async function Greeting() {
await connection();
return <p>GREETING is: {process.env.GREETING}</p>;
}

macOS-style code window titled app/page.tsx showing a Home page that wraps a Greeting component in Suspense, and Greeting awaiting connection from next/server before reading process.env.GREETING

CodeWhat it does
await connection()Waits until there is a real request. Everything after it runs for each request, on the running server
<Suspense>With Cache Components on, Next.js asks for the waiting part to be inside a Suspense boundary. The rest of the page is still prepared ahead and sent at once
process.env.GREETINGRead after the wait, so it comes from the server's environment at that moment

Step 3: Build With One Value, Start With Another​

Build with GREETING set to one value:

Terminal
GREETING='Hello from the build' npm run build

The end of the build output already shows the difference:

Output
Route (app)
┌ ◐ /
├ ○ /_not-found
└ ○ /at-build


○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content

/at-build is ○ Static: built once, with its value inside. / is ◐ Partial Prerender: its frame was built ahead, and the part after connection() runs for each request.

Now start the same build with a different value:

Terminal
GREETING='Hello from the server' npm start

Step 4: Compare the Two Pages​

http://localhost:3000/at-build still shows the value from the build:

The page Without connection showing GREETING is: Hello from the build, although the server was started with a different value

http://localhost:3000 shows the value the server is running with:

The page With connection showing GREETING is: Hello from the server, the value the server was started with

One build, started with another value, and only the page with connection() follows it.

npm run dev hides the difference​

In development, npm run dev renders every page on request. Started with GREETING='Hello from npm run dev', both pages showed that value. The problem only appears in a production build — which is exactly why it is easy to miss.

Common Mistakes​

MistakeWhat happensDo this instead
Reading process.env in a page without connection()The value is fixed at build time in productionRead it after await connection()
Calling connection() without a Suspense boundary around the componentnpm run build fails: Next.js encountered uncached or runtime data during prerenderingWrap the component in <Suspense>
Prefixing the setting with NEXT_PUBLIC_Next.js copies the value into the browser's JavaScript at build timeKeep server settings without the prefix
Testing only with npm run devBoth versions look rightCheck with npm run build and npm start

Troubleshooting​

SymptomLikely causeFix
The page shows an old value after changing a settingThe page was prerendered: the build output marks it ○Read the setting after await connection(), then build again
The page shows nothing where the value should beThe setting isn't set where the server runsSet it in the hosting platform or in .env.local
npm run build fails with Route "/": Next.js encountered uncached or runtime data during prerendering and connection() accessed outside of <Suspense>The component that awaits connection() isn't inside <Suspense>Move the reading into its own component and wrap it in <Suspense>, as in Step 2

FAQ​

Does this work for metadata such as the page title?​

Yes. generateMetadata can await connection() too, and then read the setting.

Is it slower?​

Only the part after connection() runs for each request. With Partial Prerender, the rest of the page is still built ahead and sent at once.

What about Route Handlers?​

A Route Handler that reads the request — like a POST handler — already runs for each request.

Conclusion​

In a production build, Next.js runs pages ahead of time unless they depend on the request, and process.env doesn't count. Put the code that reads a setting after await connection(), inside <Suspense>, and the same build shows whatever the server runs with — check it with npm run build and npm start, because npm run dev hides the difference.

Additional Resources​