
To deploy a Next.js app to Vercel, push your repo to GitHub, import it at vercel.com/new, set environment variables, and click Deploy. Vercel auto-detects Next.js, builds with the right settings, and gives you a production URL plus a preview URL per pull request. The "pro" part is everything that comes after: ISR, on-demand revalidation, custom domains, cron, KV, and knowing when the bill is about to hurt.
Most "deploy to Vercel" tutorials stop at step three. They show the import flow and the green build log, then leave you to discover bandwidth overages and function-invocation pricing at 2 a.m. on launch day. This is the playbook a senior engineer hands a junior on day one: the things Vercel rewards, the four billing dimensions that bite, and the precise moment when Render, Cloudflare Pages, or Fly starts making sense.
Three things changed since 2023. Next.js 15 made the App Router and Server Actions the default, which reshapes caching. Vercel split function billing into three dimensions (invocations, CPU time, memory time) instead of one, so apps that used to cost $20 now cost $80 without warning. And Edge Config plus on-demand revalidation made redeploys for trivial changes obsolete.
Push the repo to GitHub, GitLab, or Bitbucket. Import at vercel.com/new. Vercel detects Next.js automatically and sets:
next build.nextpnpm install (or whatever your lockfile implies)Two non-default things to set on day one. Pin the Node.js version under Project Settings > Node.js Version (use 22.x or whatever your package.json engines field declares; production drift is the most common build break we see). Then enable "Skip deployments for unaffected projects" if you're in a monorepo, because every PR rebuilding every app racks up Turbo build minutes fast.
A useful starter vercel.json:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"regions": ["iad1"],
"buildCommand": "pnpm turbo run build --filter=web",
"installCommand": "pnpm install --frozen-lockfile",
"git": {
"deploymentEnabled": {
"main": true
}
}
}
The regions field pins SSR functions to a single region. For a US-first product with a Postgres in us-east-1, locking to iad1 (Washington DC) cuts cold-start tail latency more than running globally and round-tripping to a single database does. Multi-region only helps if your data is also multi-region.
Vercel ignores .env.local. You set values under Project Settings > Environment Variables, scoped to Production, Preview, and Development. Three habits make this painless.
Use vercel env pull .env.local to sync values down for local dev. Group secrets by environment so a Stripe test key never leaks into production. And never put NEXT_PUBLIC_ in front of anything you'd be embarrassed to see in view-source, because that prefix bakes the value into the client bundle at build time.
For multi-tenant apps, Vercel exposes VERCEL_ENV, VERCEL_URL, and VERCEL_GIT_COMMIT_SHA automatically. Pipe them into Sentry releases or a deploy webhook to Slack, the same pattern we use when wiring GitHub Actions to a Next.js app.
Every push to a non-production branch gets a unique preview URL of the form your-app-git-branch-team.vercel.app. Every PR gets a comment with that URL. This is the single best feature of the platform.
Two things to wire up. First, password-protect previews under Settings > Deployment Protection (Pro plan and up); without this, indexers and bots crawl your unfinished work. Second, add a READY deployment hook that pings your QA Slack channel when a preview is ready, because waiting for the "deployment succeeded" GitHub comment loses 90 seconds per cycle.
If you use Cypress or Playwright in CI, point E2E runs at the preview URL instead of localhost. You'll catch SSR, ISR, and edge bugs that local dev hides.
Add the domain under Project Settings > Domains. Vercel issues a Let's Encrypt certificate within 60 seconds of DNS resolving. For apex domains (example.com), use an A record to 76.76.21.21. For subdomains, use a CNAME to cname.vercel-dns.com.
If you're proxying through Cloudflare, set the orange cloud to "DNS only" (gray cloud) on the Vercel records. Otherwise you double-CDN the request, fight over caching headers, and burn extra origin pulls. Vercel's KB article on this is unusually direct: most teams should pick one CDN, not both.
Always provision both example.com and www.example.com and set one as the canonical redirect. Splitting traffic between the two tanks SEO and confuses analytics.
Incremental Static Regeneration is the killer feature. You get static-page performance with dynamic content, and Vercel propagates updates globally in roughly 300ms.
Time-based, in the App Router:
// app/blog/[slug]/page.tsx
export const revalidate = 60; // seconds
export default async function Page({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 60, tags: [`post:${params.slug}`] },
}).then(r => r.json());
return <Article post={post} />;
}
On-demand revalidation, triggered from a CMS webhook:
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextRequest } from "next/server";
export async function POST(req: NextRequest) {
const secret = req.headers.get("x-revalidate-secret");
if (secret !== process.env.REVALIDATE_SECRET) {
return new Response("unauthorized", { status: 401 });
}
const { slug } = await req.json();
revalidateTag(`post:${slug}`);
return Response.json({ revalidated: true });
}
Always tag-based, not path-based. Tags compose; paths don't. When a post update should also invalidate the index page and the RSS feed, you call revalidateTag('post:slug') plus revalidateTag('post:list') and you're done. Path-based revalidation would have you maintain a list of every URL that mentions that post, which falls apart the moment marketing adds a new module.
Edge Config is a globally replicated read-only key-value store that reads in under 15ms at the edge. Use it for feature flags, kill switches, A/B test assignments, and short allow-lists. Don't use it for user data; it's read-mostly and writes propagate slowly.
// middleware.ts
import { get } from "@vercel/edge-config";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(req: NextRequest) {
const inMaintenance = await get<boolean>("maintenance");
if (inMaintenance && !req.nextUrl.pathname.startsWith("/maintenance")) {
return NextResponse.redirect(new URL("/maintenance", req.url));
}
return NextResponse.next();
}
export const config = { matcher: "/((?!_next|api|maintenance).*)" };
A maintenance toggle that used to require a redeploy is now a single API call. The first time you ship a kill switch this way and use it during an incident, you'll wonder how you lived without it.
next/image with Vercel optimizes on demand, caches transformations, and serves AVIF / WebP based on the Accept header. The cost trap: every unique transformation counts, so a thumbnail and a hero variant of the same source are two transformations.
Whitelist remote sources in next.config.js:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.example.com" },
{ protocol: "https", hostname: "*.supabase.co" },
],
formats: ["image/avif", "image/webp"],
minimumCacheTTL: 86400, // 1 day; default is 60 seconds
},
experimental: {
ppr: "incremental",
},
};
module.exports = nextConfig;
Bumping minimumCacheTTL from the 60-second default to a day cuts re-optimization cost by 90% on a content-heavy site. Most teams never touch this and overpay accordingly.
Install both tracking packages in the App Router root layout:
// app/layout.tsx
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
);
}
Speed Insights captures real-user LCP, INP, and CLS per route. Set a budget and alert on regression: LCP > 2.5s on any route shipped that week is a P1. The team that ignores this for six months ends up with a 4-second LCP and no idea where it came from. Pair this with OpenTelemetry for backend traces and you have a full picture from browser to database. If you also need to track render performance issues at the component level, optimizing React performance with the React Compiler closes the loop.
Vercel Cron runs scheduled HTTP calls against your routes. Defined in vercel.json:
{
"crons": [
{
"path": "/api/cron/daily-digest",
"schedule": "0 13 * * *"
},
{
"path": "/api/cron/cleanup",
"schedule": "*/15 * * * *"
}
]
}
The handler should verify the request came from Vercel:
// app/api/cron/daily-digest/route.ts
import { NextRequest } from "next/server";
export async function GET(req: NextRequest) {
if (req.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response("unauthorized", { status: 401 });
}
// ... do the work
return Response.json({ ok: true });
}
Hobby plan caps at two crons; Pro at 40. If you need second-level precision or guaranteed-once delivery, this is where you outgrow Vercel Cron and adopt Inngest, Trigger.dev, or a Cloudflare Workers cron.
Vercel resells Upstash (KV / Redis), Neon (Postgres), and provides Blob storage. The integration is one click and the UX is excellent for prototypes. The price is the catch.
| Storage | Vercel-branded price | Direct provider price | Verdict |
|---|---|---|---|
| KV / Redis | Upstash + 30-50% margin | Upstash direct, same SLA | Use Upstash directly past prototype |
| Postgres | Neon + Vercel margin | Neon direct, identical Postgres | Use Neon direct, or Supabase / Render |
| Blob | $0.15/GB stored, $0.30/GB egress | R2: $0.015/GB stored, $0 egress | R2 wins for any media workload |
For prototypes and quick MVPs, the integrated storage is worth the markup. For anything past 10k users, click out to the provider's direct console and migrate the connection string. You'll halve the bill with no architectural change.
Vercel's pricing page shows $20/seat/month for Pro and looks reasonable. The bill at the end of the month rarely is. Four dimensions stack:
| Dimension | What you pay | When it bites |
|---|---|---|
| Bandwidth | $0.15 / GB after 1 TB Pro allowance | High-traffic content sites, image-heavy apps |
| Function invocations | $0.60 per million, plus $0.128 / CPU-hour, plus $0.0106 / GB-hour | SSR-heavy dashboards, every request hits a function |
| Image transformations | Per unique transformation past free tier | Dynamic resizing on product images / avatars |
| Build minutes | $0.126 / minute on Turbo (Feb 2026 default) | Monorepos, frequent PRs, slow builds |
A real example we audited: a Next.js dashboard with 25 PRs/month and ~50k MAU was billed $415 in build minutes alone before any traffic costs. The fix was a Turborepo cache hit rate of 70% and pruning preview deploys to relevant apps only. Bill dropped to $80.
There are no hard spend caps. A misconfigured cron, a viral tweet, or a runaway loop can bill four figures overnight, and Vercel will cheerfully process it. Set a billing alert on day one.
Vercel is the right answer for most apps under $1M ARR. The math flips in three scenarios:
You don't have to go all-or-nothing. The most common production pattern we see in 2026: Next.js front door on Vercel for the marketing site and dashboard, long-running jobs on Render or Fly, static media on Cloudflare R2, Postgres on Neon directly. Each piece on the platform that prices it best.
runtime = 'edge' on routes that don't need Node, then paying Node pricing for trivial JSON responses.revalidatePath('/') from a webhook and crashing the cache for every visitor in the same second.next dev against production env vars and writing to the production database by accident.output: 'standalone' in next.config.js if you ever plan to self-host alongside Vercel.If you're stuck mid-rollout (ISR not invalidating, Server Actions misbehaving in production, build minutes spiraling) the work is small but specialized. Cadence's senior tier ($1,500/week) routinely owns Next.js production hardening: deploy pipelines, monitoring, cache invalidation, and the cost-trim audit. Every engineer on the platform is AI-native, vetted on Cursor and Claude Code fluency before they unlock bookings, so they ship the first PR fast. Median time to first commit across our 12,800-engineer pool is 27 hours.
If you'd rather audit the stack first, our free ship-or-skip stack audit grades your Vercel + Next.js setup against this checklist and tells you which of the four cost dimensions you're already triggering. It takes 90 seconds.
vercel env pull.vercel.json with regions, buildCommand, and git.deploymentEnabled to control where and when functions run.export const revalidate = N, and wire on-demand revalidateTag from your CMS webhook.middleware.ts first.next.config.js with images.remotePatterns and bump minimumCacheTTL to 86400.@vercel/analytics and @vercel/speed-insights in the root layout. Set Web Vitals budgets in CI.crons block to vercel.json and verify Bearer token in handlers.Auditing your Vercel + Next.js setup before scale matters more than picking a cheaper alternative. Cadence's ship-or-skip stack grader returns an honest report card in 90 seconds, and you can book a senior engineer for a focused 48-hour deploy hardening sprint at $1,500/week if the report flags real issues.
Three to five minutes if your repo is on GitHub already. The Vercel import flow auto-detects Next.js, runs next build, and gives you a URL. The 10-step hardening playbook above takes another two to four hours, mostly spent writing config and wiring monitoring.
vercel.json file at all?No. Vercel auto-detects every default. You add vercel.json when you want to pin regions, define cron jobs, set custom headers, override the build command, or enable git-deployment rules. For a typical Next.js app shipping today, you'll add it within the first week.
Use tag-based revalidation, set minimumCacheTTL on images to 24 hours or more, and route static assets through Cloudflare R2 (zero egress) instead of Vercel Blob. ISR cache hits don't count as function invocations, so the bill stays under control as long as your hit rate is high. Watch for cache stampedes after a global revalidate.
Server Actions shine for forms and mutations co-located with the page that triggers them. They reduce round-trip code and remove a layer of API plumbing. For shared logic across multiple frontends, mobile apps, or third-party consumers, a dedicated API route or REST endpoint still wins. We cover the tradeoffs in the Server Actions Next.js 15 guide and our REST API design playbook.
Yes, but be careful. Run migrations in a separate one-off step before the deploy promotes traffic, not as part of the build. The cleanest pattern: a predeploy GitHub Action that runs drizzle-kit migrate against the production database, then triggers the Vercel build via a deploy hook. Migrating inside the build couples schema changes to deploy success and creates ugly rollback scenarios.
When your monthly bill crosses $2,000, when a single function bills more than $200/month, or when you hit the 300-second function timeout. Below that threshold, the platform pays for itself in shipped features. Above it, do the math on Render, Cloudflare Pages, or Fly. Our Vercel vs Cloudflare Pages comparison and Vercel pricing review walk through the migration math.