Skip to content

Deploying Next.js to Vercel


1. First Deployment

Via the Dashboard

  1. Import the Git repository
  2. Vercel detects Next.js and fills in the build settings
  3. Add environment variables
  4. Deploy

Via the CLI

bash
npm i -g vercel

vercel          # deploy a preview
vercel --prod   # deploy to production
vercel link     # connect a local folder to an existing project
vercel env pull # download env vars into .env.local

vercel env pull is the useful one day to day — it keeps your local .env.local in sync with the dashboard.


2. Build Settings

SettingDefault for Next.js
Framework presetNext.js (auto-detected)
Build commandnext build
Output directory.next
Install commandnpm ci (if package-lock.json exists)
Node versionSet in project settings or engines in package.json

Pin Your Node Version

json
{
  "engines": { "node": "20.x" }
}

Without this, Vercel may upgrade the default Node version and your build behaves differently than it did last week. Next.js 16 requires Node 20.9+.

Monorepos

Set the Root Directory in project settings (e.g. apps/web). For Turborepo, use turbo build --filter=web as the build command and enable Ignored Build Step so a change to the API package doesn't rebuild the web app.


3. How Next.js Maps to Vercel

Next.js conceptOn Vercel
Static pages / "use cache" shellFiles on the global CDN
Server Components (uncached)Serverless function
Route HandlersServerless function
Server ActionsServerless function (POST endpoint)
proxy.ts / middlewareEdge, runs before everything
next/imageVercel's image optimisation service
ISR / cached contentVercel's durable cache, CDN-backed
/publicCDN

Reading the build output tells you which route became what:

○  (Static)   prerendered
●  (SSG)      prerendered with generateStaticParams
ƒ  (Dynamic)  server-rendered on demand

An ƒ where you expected means something opted the route into dynamic rendering — usually cookies(), headers(), or an uncached fetch outside a Suspense boundary.


4. Environment Variables

Vercel has three scopes: Production, Preview, Development.

DATABASE_URL          → different value per scope
NEXT_PUBLIC_API_URL   → different value per scope
STRIPE_SECRET_KEY     → test key for Preview, live key for Production

The Two Rules

1. NEXT_PUBLIC_ is public and build-time. It is inlined into the JavaScript bundle. Anyone can read it in DevTools, and changing it in the dashboard does nothing until you redeploy.

2. Everything else is server-only and runtime. Safe for secrets, and a redeploy picks up changes.

Preview Should Not Touch Production Data

Give Preview its own database and its own third-party test keys. A preview deployment is a full working app — if it points at production, a reviewer clicking around can delete real records.

System Variables

js
process.env.VERCEL_ENV;         // "production" | "preview" | "development"
process.env.VERCEL_URL;         // this deployment's URL
process.env.VERCEL_GIT_COMMIT_SHA;

Useful for tagging errors in Sentry with the exact commit.


5. Caching on Vercel

Next.js 16 flipped caching to opt-in. On Vercel:

  • "use cache" results are stored in Vercel's durable cache and served from the CDN
  • Uncached Server Components run per request in a function
  • revalidateTag / updateTag purge across the whole CDN, globally

Cache Is Scoped to a Deployment

The cache key includes the build ID, so a new deployment starts with a cold cache. That's deliberate — it stops a new build serving markup produced by the old one.

Practical effect: the first requests after a deploy are slower. Don't mistake it for a regression.

Manual Cache Headers

ts
return NextResponse.json(data, {
  headers: { "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300" },
});

s-maxage targets the CDN; stale-while-revalidate lets it serve the stale copy while fetching a fresh one in the background. For a Route Handler that returns semi-static data, this is often all you need.


6. Serverless Function Limits

LimitTypical value
Execution timeout10s Hobby, 60s Pro (configurable up to 300s)
Memory1024 MB default
Payload size4.5 MB request body
Response size4.5 MB (streaming exempt)
Bundle size250 MB unzipped
ts
export const maxDuration = 60;   // per route

Working Around Them

ProblemSolution
Job takes minutesA queue (Inngest, QStash, BullMQ on a separate worker)
Large file uploadPresigned URL direct to S3/R2 — bypass the function entirely
Large responseStream it
Heavy PDF/image generationA separate service, or a background job

The presigned URL pattern is worth knowing: the client uploads straight to object storage, and your function only issues the signed URL. No 4.5 MB limit, no function time spent proxying bytes.


7. Database Connections

The failure that catches everyone.

Traffic spike → 500 concurrent function invocations
             → 500 database connections
             → Postgres max_connections = 100
             → "too many clients already"

Fixes

OptionHow
Connection poolerPgBouncer, Prisma Accelerate, Neon pooler, Supabase pooler
HTTP-based driverNeon serverless, PlanetScale, Upstash — no persistent TCP
Reuse the clientCache the client on globalThis so warm invocations share it
ts
// lib/db.ts — prevents a new client per hot reload in dev, and reuses on warm invocations
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const db = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;

Important nuance: the globalThis trick helps with warm invocations and dev hot reload. It does not solve the cold-start fan-out — you still need a pooler for real traffic.


8. Rollback

Every deployment has a permanent URL. Production is a pointer.

Dashboard → Deployments → pick a previous one → Promote to Production.

Takes seconds and involves no rebuild — the artifact already exists.

What Does Not Roll Back

Database migrations. Code reverts in seconds; a dropped column does not come back. Run destructive migrations as a separate, later deploy, after the new code has proven stable.


9. Debugging a Failed Deploy

Read the Build Log First

ErrorCause
Module not found: './Button'Case-sensitive imports — Linux builds, macOS/Windows dev. The most common one
Cannot find module 'x'Dependency in devDependencies but needed at build
process.env.X is undefined at buildEnv var not set for that scope
Type errorsVercel runs the real build; local next dev skips type checking
Out of memoryLarge builds — reduce parallelism or upgrade the plan
Dynamic server usageA route used cookies()/headers() where a static render was expected

Runtime Logs

Dashboard → Deployment → Functions → Logs. Or vercel logs <url>.

Reproduce Locally

bash
npm run build && npm start   # matches production far better than next dev

next dev skips type checking, uses a different bundler configuration, and does not exercise the static/dynamic split. A large share of "works locally, breaks on Vercel" issues are visible with a local production build.


10. Performance on Vercel

The Checklist

  • [ ] next/image with correct sizes, priority on the hero
  • [ ] next/font — no external font request, no layout shift
  • [ ] Static or "use cache" for anything not personalised
  • [ ] "use client" pushed as low in the tree as possible
  • [ ] next/dynamic for heavy browser-only components
  • [ ] First Load JS reviewed per route in the build output
  • [ ] Speed Insights enabled

Compute Near Data

Set the function region to match your database region. A function in iad1 querying a database in iad1 is a millisecond away; the same function in sin1 is 200ms away, per query.

Edge functions are globally distributed, which sounds better and is often worse for anything that touches a database. Use Edge for logic that needs no data access — auth token checks, redirects, geolocation, A/B assignment.

Analytics

tsx
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";

<body>
  {children}
  <Analytics />
  <SpeedInsights />
</body>

Speed Insights reports real-user Core Web Vitals, which is what Google ranks on — not Lighthouse's synthetic run on a simulated device.

© 2025 DDocs · Dipak's Documentation Guide