
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.

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#
- In vercel/next.js issue 87071, the report says production returned "
HTTP 500 Internal Server Error" while dev worked. - In vercel/next.js issue 61923, the cache was "only purged when the app is rebuilt."
- In Stack Overflow 65383169, the production env value was "
undefined". - In Stack Overflow 65058598, the symptom was: "works fine when I run it locally."
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:
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 redeployedWriting 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:
Browser symptom -> Network response -> Vercel function logs -> runtime -> env vars -> cache headersThat 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:
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:
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.
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:
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.
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.
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 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:
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:
"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:
- Confirm the failing URL and method.
- Check browser Network status, response body, and response headers.
- Check Vercel runtime logs for that request.
- Identify runtime: browser, Edge, Node.js serverless, static, or ISR.
- Print safe env presence booleans, never secret values.
- Check cache headers and route segment config.
- Reproduce with
next buildandnext start, but do not treat that as equivalent to Vercel.
Use targeted logging:
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.
- 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.
Related#
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Continue Reading

Next.js Middleware Not Running on Vercel: 3 Causes
Middleware that works locally can disappear in Vercel production because the matcher never matches, Edge bundles a Node-only import, or auth middleware cannot read the token. This guide gives you the diagnosis order I use before touching app code.
How to Access Route Parameter Inside getServerSideProps
Learn why your dynamic route parameter appears as undefined in getServerSideProps and how to correctly extract it from the context object for server-side data fetching.
Fix Module not found: Can't resolve 'encoding' in Vercel
You see "Module not found: Can't resolve 'encoding'" in your Vercel deployment logs. This guide explains why it happens with cross-fetch/node-fetch and how to fix it permanently.
Browse by Topic
Find stories that matter to you.
