Next.js on Vercel
Your Next.js app works locally and breaks on Vercel
Your app runs cleanly under next dev and falls over on Vercel. That is not bad luck: development and production are two different programs. Locally, every page renders on demand, every query resolves in a millisecond and everything shares one Node process. Deployed, your routes are frozen at build time or split across serverless functions and runtimes with real latency between them. The six failures below account for most broken Next.js deploys, each with the mechanism behind it.
Fixed £4,500 · 3 days · pass-or-refund · next slot w/c 3 August
What actually breaks
- The page was rendered once, at build timeAt build, Next.js decides for every route whether it is static or dynamic. If a page never touches cookies, headers or another request-time API, it is prerendered: the data fetch runs once on the build machine and the resulting HTML is served to everyone until the next deploy. In development every page renders on demand, so the same code looks live on your laptop. That is why the dashboard shows Tuesday's numbers. The page is not slow to update; it was never going to update.
- The write worked, but nothing told the cached pageContrary to its reputation, Next.js does not cache your data fetches by default: the docs are plain that fetch requests are not cached unless you opt in. What holds the stale value is the prerendered route from the break above, plus anything opted into force-cache or unstable_cache. A mutation must call revalidatePath or revalidateTag afterwards, and AI tools reliably write the update and forget the invalidation. Development will not warn you: next dev caches fetch responses across hot reloads, so even your laptop can show stale data.
- Props carry the whole database row to the browserEvery prop passed from a server component to a client component is serialised and sent to the browser in the page's data payload. Hand a profile card the whole user row and every column travels: password hash, Stripe customer id, internal flags, whether the template renders them or not. This is not code in the bundle; it happens per request, in the response stream, readable in the network tab. Next.js's own security guide labels the pattern exposed. Generators produce it constantly, because passing the whole object type-checks cleanly.
- A deploy can break tabs your users already have openVariables an inline server action captures are encrypted with a key generated at each build; Next.js says actions can only be invoked for a specific build. Action IDs are also regenerated on a fresh build or when the build cache is invalidated. A tab loaded before the deploy that then submits an affected form gets a server error; tabs that never invoke one are untouched. Vercel's skew protection pins stale clients to their deployment, on by default for projects created since November 2024; older projects enable it in settings.
- Your function lives in Washington; your data does notBy default a new project's functions all run in one region: iad1, in Washington DC. Put your database in London and every query crosses the Atlantic twice; sequential awaits that cost a millisecond on your laptop become a visible waterfall. Without a Suspense boundary or a loading file, Next.js holds back the whole response until the slowest fetch resolves, so users watch a blank tab. And the wait has a ceiling: run past the function's maximum duration, 300 seconds by default, and Vercel terminates it mid-response.
- On Next 14 and 15, middleware runs in a smaller runtimeOn Next 14 and 15, middleware.ts runs on the Edge runtime: no filesystem, only part of Node's standard library, so a database client or JWT package that works everywhere else throws only here. Next 16 renamed the file to proxy and runs it on Node by default, so check your version. Two traps survive: without a matcher, middleware runs on every request, your CSS and images included; and server actions are POST requests to the page's own path, so a matcher that excludes a route strips its actions too.
How to see what production sees
Most of this surfaces the moment you stop trusting next dev. Run next build and read the route table: it marks every route static or dynamic, and a static marker next to a page that should show live data is the first break found in thirty seconds. Run next start and click through with the network tab open; the payload shows exactly what crossed the server boundary. On Vercel, the function logs record region, duration and terminations for every invocation, and the x-vercel-cache header on each response tells you which cache answered. None of this needs new code. It needs someone to read what the build and the platform are already reporting, then make each rendering and caching decision deliberate.
What “done” looks like
We agree one of these in writing before you pay anything. If it passes, the sprint passed. If it doesn't, you get a full refund and keep the work.
- The build route table matches intent: live pages render dynamically, marketing pages stay static, and every choice is deliberate rather than accidental.
- Every mutation calls revalidatePath or revalidateTag for the data it changed, verified by watching the page update after a write in a production build.
- Nothing crosses the server-to-client boundary except fields the UI renders, verified by reading the actual response payload, not the component code.
- Functions run in the same region as the database, slow fetches stream behind Suspense boundaries, and skew protection is confirmed on in project settings.
We agree the acceptance test before any work starts. If it doesn't pass within three working days, you get a full refund and keep every change I've committed. The risk is mine, not yours.