Next.js Build Passed But Production Broke
Next.js

Next.js Build Passed But Production Broke

A green build only proves the bundle compiled. This postmortem walks through three production-only failures: missing Vercel env vars, Edge runtime imports, and cache behavior that changed after deploy.

2026-06-17
8 min read
Next.js Build Passed But Production Broke

Introduction#

A passing Next.js build does not mean production is healthy. It means the compiler produced an artifact; it does not prove Vercel has the right env vars, the Edge bundle can run, or your cache behavior matches local testing.

This is a composite postmortem built from the public reports below and the production checks I would run on the same symptoms. It is not presented as a private outage with invented users or metrics.

If this sounds familiar, keep Vercel env variable fixes, module not found during Next.js builds, and Next.js stale cache revalidation fixes open while you debug.

Real Reports This Postmortem Is Based On#

What Happened#

The build passed. The preview URL loaded. The homepage looked normal.

In the composite incident, the deployed app shows three failures:

  • Authenticated pages redirected to login.
  • A proxied analytics script returned 500.
  • A dashboard list showed stale data after updates.

No single error explained all three. That was the clue. When production breaks in multiple unrelated places after a green build, assume environment, runtime, or cache boundaries before assuming a React component bug.

Timeline#

The useful timeline is not "deploy happened, site broke." It is more specific:

txt
09:12 deploy completed
09:18 first login loop report
09:24 analytics proxy 500 found in Network tab
09:31 stale dashboard reproduced with a fresh account
09:44 env var mismatch confirmed in Vercel Production
10:06 Edge runtime import isolated
10:22 cache config fixed and redeployed

Writing the timeline keeps the symptoms separate. The login loop, 500, and stale dashboard start after the same deploy, but they do not share the same root cause.

After that, debugging became mechanical: one symptom, one runtime, one cause.

What We Tried First#

The first instinct was to redeploy. It did nothing.

The second instinct was to clear .next locally and rebuild. It passed again.

The third instinct was to compare package versions. Nothing obvious changed.

Those checks were not useless, but they were too broad. The better move was to split the incident by runtime:

txt
Browser symptom -> Network response -> Vercel function logs -> runtime -> env vars -> cache headers

That produced three root causes.

Root Cause 1: Production Env Vars Were Missing#

Local .env.local had everything. Vercel production did not.

That is how you get code like this passing build:

ts
const mapsKey = process.env.NEXT_PUBLIC_GOOGLEMAPS;

But production logs show the value is undefined at runtime or the browser receives an empty public key.

The fix was not to commit env files. The fix was to audit Vercel's environment scopes:

  • Development
  • Preview
  • Production

Then redeploy after adding the missing production values.

For server-only values:

ts
function requiredEnv(name: string) {
  const value = process.env[name];
 
  if (!value) {
    throw new Error(`Missing required env var: ${name}`);
  }
 
  return value;
}
 
export const STRIPE_SECRET_KEY = requiredEnv("STRIPE_SECRET_KEY");

For public values, remember that NEXT_PUBLIC_ values are bundled for the browser. If you change them in Vercel, rebuild and redeploy.

Don't
Testing with .env.local and assuming Vercel Production has the same values
Do
Audit Vercel Production env vars and redeploy after changes

Root Cause 2: Edge Runtime Imported Node Code#

The analytics proxy worked in development and failed in production. The route looked harmless because the Node-only import was behind a branch. But Edge bundling does not care that the branch usually does not run.

Bad:

ts
import Redis from "ioredis";
 
export const runtime = "edge";
 
export async function GET() {
  const redis = new Redis(process.env.REDIS_URL);
  return Response.json({ ok: true });
}

Fix one: run the route in Node.js if it needs Node APIs.

ts
export const runtime = "nodejs";

Fix two: if this is middleware, do not use Node-only libraries there at all. Middleware is Edge. Move the database or Redis operation into a route handler and let middleware make only a cheap redirect/rewrite decision.

ts
export function middleware(request) {
  if (!request.cookies.has("session")) {
    return Response.redirect(new URL("/login", request.url));
  }
}

This is the same class of problem as middleware auth failures; see Next.js middleware not running on Vercel for the full flow.

The Fix

The fix in one line: Edge code must import Edge-safe dependencies, even when Node-only code is behind a conditional branch.

Root Cause 3: Cache Behavior Was Different After Deploy#

The stale dashboard was not a failed database write. The write succeeded. The page kept rendering cached data.

In App Router, fetch caching, route segment config, revalidate, revalidatePath, and Vercel's production cache can interact in ways that local dev hides.

If the page must always show fresh authenticated data, make that explicit:

tsx
export const dynamic = "force-dynamic";
 
export default async function DashboardPage() {
  const data = await fetch("https://api.example.com/dashboard", {
    cache: "no-store",
  }).then((res) => res.json());
 
  return <Dashboard data={data} />;
}

If the page can cache but needs invalidation after a mutation, revalidate the exact path:

ts
"use server";
 
import { revalidatePath } from "next/cache";
 
export async function updateProject(input) {
  await db.project.update(input);
  revalidatePath("/dashboard/projects");
}

Then verify in production with response headers and logs. Local next dev is intentionally not a perfect cache simulator.

The Debugging Checklist We Kept#

When a green build ships a broken production app, run this order:

  1. Confirm the failing URL and method.
  2. Check browser Network status, response body, and response headers.
  3. Check Vercel runtime logs for that request.
  4. Identify runtime: browser, Edge, Node.js serverless, static, or ISR.
  5. Print safe env presence booleans, never secret values.
  6. Check cache headers and route segment config.
  7. Reproduce with next build and next start, but do not treat that as equivalent to Vercel.

Use targeted logging:

ts
console.log("prod-debug", {
  route: "/api/checkout",
  hasStripeKey: Boolean(process.env.STRIPE_SECRET_KEY),
  runtime: process.env.NEXT_RUNTIME,
});

Remove it after the incident.

Prevention#

Add env validation during startup for required server variables. Add a small production smoke test that hits the auth callback, one authenticated page, one API route, and one cache-sensitive page after deploy.

For routes that must not run on Edge, declare runtime = "nodejs". For middleware, keep imports boring and small.

For pages that must not cache, declare that explicitly. For pages that should cache, write the invalidation path next to the mutation.

When this won't work
  • If a third-party API is down, a Next.js build cannot catch that.
  • If production data differs from staging data, local reproduction may be misleading.
  • If secrets are rotated outside Vercel, redeploys alone will not fix invalid credentials.

Summary#

  • A green build proves compilation, not runtime correctness.
  • Missing Vercel production env vars can pass local and preview checks.
  • Edge runtime failures often come from Node-only imports.
  • Cache bugs can look like failed writes when the data actually changed.
  • Production smoke tests should cover env, auth, runtime, and cache-sensitive routes.

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.