How a git push becomes a globally distributed deployment. The CDN topology, Fluid compute concurrency model, environment isolation, and every decision point where latency or cost accumulates.
Vercel is a deployment platform built around the idea that the hosting layer should understand your framework. When you push to GitHub, Vercel does not just copy your files to a server. It reads your framework's output format, decides what becomes a CDN-cached static file, what becomes a serverless function, and where in the world each piece lands. That distinction drives everything downstream: latency, cost, scaling behavior, and debugging.
Most developers interact with Vercel through preview URLs and production deploys. The architecture underneath those interactions involves a global CDN with 126 or more Points of Presence across 51 countries, 20 or more compute regions, a build system that turns framework output into deployment artifacts, and Fluid compute, the execution model that replaced classic one-request-per-instance serverless in 2025.
126+ PoPs across 51 countries. Static assets served from the closest edge node. Zero TTL configuration needed for framework-aware deploys.
Default since April 2025. Multiple requests run concurrently on one function instance. Reduces cold starts through bytecode caching and pre-warming.
Next.js, SvelteKit, Nuxt, Remix, and more are understood at the build layer. Routing, caching, and rendering strategy read from framework output.
Three environments (Production, Preview, Development) with distinct env var scopes. Every branch gets a preview URL before touching production.
Vercel operates as two layers. The CDN layer handles routing, caching, and static assets at the edge. The compute layer handles functions, SSR, and dynamic routes in specific regions. A request hits the edge first. If the response is cached, it never reaches compute. If it needs computation, the edge proxies to the nearest function region. Knowing which layer handles each request is the foundation for debugging performance.
Vercel's CDN is not a traditional CDN bolted onto a hosting service. It is the routing layer that all traffic passes through, including dynamic responses. The network consists of PoPs (Points of Presence) that serve cached content and route requests to compute regions when computation is required.
These are two different tiers. A PoP is a lightweight edge node that terminates TLS, checks caches, applies routing rules, and serves static content. There are 126+ of them, spread thin across the globe. A compute region is a full data center that runs your functions. There are 20+ of them, concentrated in major cloud provider zones (AWS, GCP).
Static assets hit the PoP and return. Dynamic functions execute in a compute region. The edge proxies the request to the nearest region that has your function deployed. This is why pinning a function to iad1 (Northern Virginia) when your database is in eu-central-1 produces ~100ms of unnecessary round-trip on every DB query.
Traditional CDNs require manual cache-control header configuration. Vercel reads the framework's build output format, called the Build Output API, and sets cache policies automatically. A Next.js static page gets a long-lived CDN cache. A dynamically rendered route gets no-store. ISR routes get stale-while-revalidate headers matched to the revalidate config.
Every deploy is scoped to a branch. CDN configuration changes are previewed on a branch URL before reaching production. There is no separate CDN config file to maintain.
Vercel runs an availability zone failover model. If a function instance in one AZ goes down, traffic shifts to another AZ in the same region. If the entire region goes down, traffic routes to the next closest region. This applies to both Fluid and non-Fluid deployments. The failover is automatic and opaque to application code.
A request from a browser to a Next.js app on Vercel takes a specific path. Understanding each hop explains where latency comes from and where it can be cut.
Your domain points to Vercel's anycast IP. The browser resolves the nearest PoP in milliseconds.
TLS handshake completes at the nearest PoP, not at the origin. Reduces TLS overhead for geographically distributed users.
Edge checks if a valid cached response exists. A hit returns immediately. Static assets always hit here.
Edge middleware runs at the PoP if configured. Used for auth, geo-routing, A/B headers. Runs before compute.
Cache miss + dynamic route: edge proxies to compute region. Fluid instance receives the request.
Function returns response. Edge optionally caches it (ISR / stale-while-revalidate). Returns to client.
iad1 querying a Neon database in eu-west-2 adds ~80-100ms per query. Pin functions to the same region as your data.Middleware runs at the edge PoP, before the request reaches your function. It has access to the request headers, URL, and geo-location data. Use it for redirects, auth token checks, locale detection, and A/B routing. Because it runs at the edge, it adds minimal latency (typically under 2ms for lightweight logic).
Middleware uses the edge runtime. It cannot use Node.js APIs. It cannot hold a long-lived database connection. Connecting to a database inside middleware puts you in the wrong architectural layer.
Classic serverless has a well-known problem: one request per instance. Each incoming request potentially starts a new container, waits for initialization, handles the request, and idles until the instance is recycled. This works for bursty, low-latency workloads. It breaks down for long-running tasks like streaming AI responses, where the instance sits mostly idle while tokens trickle in.
Fluid compute, which became the default for all new Vercel projects on April 23, 2025, changes this. A single function instance can handle multiple concurrent requests. The instance stays alive and warm between requests. Unused CPU capacity during one request's I/O wait is available to another request on the same instance.
Fluid compute handles multiple invocations within a single function instance. This is available on Node.js and Python runtimes. Vercel automatically manages how many concurrent requests an instance handles before scaling up to a new instance.
Errors in one concurrent request do not crash the instance or affect other requests. Fluid compute isolates uncaught exceptions per request. An unhandled promise rejection in request A does not abort request B running on the same instance.
One of the larger sources of cold start time in Node.js is parsing and compiling JavaScript. On each cold start, Node.js re-parses the function bundle from source text. Fluid compute applies V8 bytecode optimization automatically. The compiled bytecode is cached across invocations, so subsequent cold starts skip the parse step. For large Next.js app bundles, this reduces cold start time meaningfully.
Vercel Functions normally close the instance after return response. Any async work you fire after returning the response is cancelled. The waitUntil API from the @vercel/functions package signals that background work should continue even after the response is sent.
import { waitUntil } from '@vercel/functions' export async function GET(req) { // Send response immediately const result = await fetchData() // Log to analytics after response, non-blocking waitUntil(logToAnalytics(result)) return Response.json(result) }
Use this for analytics writes, cache priming, and sending notifications. The user gets the response immediately. The background task completes on the same instance without holding up the round-trip.
Fluid compute scales by first trying to fill existing warm instances with concurrent requests. Only after available concurrency is exhausted does it spin up a new instance. This means the scaling knee is higher before new cold starts occur. During a traffic spike, existing warm instances absorb more load before new ones appear. During a traffic lull, fewer instances sit idle, reducing cost.
Fluid compute has the largest impact on two types of workloads. First, AI inference proxies: requests that stream a slow upstream response (an LLM token stream) hold the instance for seconds. Under classic serverless, those seconds counted as one-request-active-one-request-blocked. Under Fluid compute, other requests run concurrently during the wait. Second, high-traffic API routes that receive bursts: Fluid absorbs concurrent requests on warm instances before triggering cold starts. For simple low-traffic routes, the difference is smaller.
Vercel Functions are the compute primitive. They accept an HTTP request, run your code, and return a response. Everything that is not a static file becomes a function on Vercel. In Next.js, that includes API routes, server components with data fetching, route handlers, and middleware.
Bun.serve as an entrypoint. Supports large functions and extended max duration on Fluid compute.fs, no crypto, no native modules. Global cold-start near zero. Tight bundle and CPU limits. Suitable for auth checks, geo routing, and lightweight transforms.| Runtime | Fluid Concurrency | Max Duration (Pro) | Node.js APIs | Use Case |
|---|---|---|---|---|
| Node.js | Yes | 800s / 1800s extended | Full | General API, SSR, AI streaming |
| Python | Yes | 800s / 1800s extended | N/A (Python stdlib) | ML inference, data processing |
| Go | No | 800s | None | High-throughput APIs, CLIs |
| Bun | Yes | 800s / 1800s extended | Compatible | Fast server-side JS, Hono |
| Edge | Inherent (stateless) | ~30s (strict) | Web APIs only | Auth middleware, geo routing |
In Next.js App Router, each route file can export config that controls how Vercel runs it. These are read at build time and baked into the deployment artifact.
// app/api/stream/route.ts export const runtime = 'nodejs' // 'nodejs' | 'edge' export const dynamic = 'force-dynamic' // no CDN cache export const maxDuration = 60 // seconds (≤ plan limit) // ISR route: regenerate every 60s export const revalidate = 60
Scheduled work is configured in vercel.json as a crons array. Each entry maps a path to a cron expression. Vercel calls that path on schedule using a standard HTTP GET. The path is a public URL, so you must protect it.
{
"crons": [
{ "path": "/api/daily-sync", "schedule": "0 9 * * 1-5" }
]
}
Vercel sends an Authorization: Bearer <CRON_SECRET> header on each scheduled call. Verify it in the handler. Without this check, any caller can trigger your cron endpoint.
Functions run in the region closest to the user by default. For data-heavy functions that query a specific database, this default is wrong. A user in Tokyo hitting a function that proxies to a Neon database in AWS us-east-1 wastes the geographic proximity advantage entirely.
Pin your function to the same region as your database. In route config: export const preferredRegion = ['iad1']. For Fluid compute projects, set the region in the project dashboard. The function runs farther from some users, but the database round-trip drops from ~150ms to ~2ms.
A Vercel deploy starts with a git push. The build pipeline takes your source and produces a build output artifact, then distributes that artifact to the CDN and compute layers.
Push to any branch. Vercel detects the push via GitHub/GitLab/Bitbucket webhook and queues a build.
Vercel inspects the repo for framework markers (next.config.ts, svelte.config.js, etc.) and selects the matching build preset.
Runs your build command (npm run build). Uses cached node_modules and build artifacts from previous deploys when available.
Reads the build output (Build Output API format). Separates static files, edge functions, serverless functions, and routing rules.
Static assets push to CDN edge nodes globally. Functions deploy to selected compute regions. Routing config propagates to all PoPs.
Branch deploy gets a unique preview URL immediately. Production branch also updates the production domain atomically.
Vercel caches node_modules and framework build artifacts between deploys. On a dependency change, the cache invalidates automatically. On a non-dependency code change, the cached node_modules accelerates the build significantly (from ~60s to ~15s for a mid-size Next.js app). If a deployment behaves unexpectedly after a dependency upgrade, clear the build cache from Settings > Git > Clear Build Cache and redeploy. The cache is project-scoped, not branch-scoped.
When a new production deploy is live, traffic shifts atomically. The old deployment stays accessible via its unique immutable URL. There is no rolling update window where some requests hit the old code and others hit the new code. Both old and new deployments coexist with separate URLs. Rolling back is switching the production alias to the previous immutable deployment URL, which completes in seconds.
Vercel has three environments, and the distinction matters more than it looks. Env vars are scoped per environment. A var set only for Production does not appear in preview deployments. A var set for both Production and Preview does not appear in local development unless explicitly pulled.
main). These deploys serve your custom domain. Vars scoped here are available only in production builds.my-app-git-feature-xyz-org.vercel.app). Vars scoped to Preview are available in all non-production deploys.vercel env pull .env.local. Available only locally. Never deployed.Any env var prefixed with NEXT_PUBLIC_ is inlined into the client-side JavaScript bundle at build time. Next.js replaces the variable reference with the literal string value. Anyone who inspects your bundle can read it. Never put tokens, API keys, or secrets in a NEXT_PUBLIC_ var. Use plain vars (without the prefix) for server-only values. They are never bundled into client code.
Changing an env var in the Vercel dashboard does not affect the running deployment. The build bakes var values into the artifact at compile time (especially NEXT_PUBLIC_ vars). To pick up the new value, trigger a new deploy. For server-only vars passed via process.env, the behavior depends on whether the value is used at build time or request time. Request-time reads get the var from the running container environment, so server-only vars can be changed with a redeploy. Build-time reads (like metadata generation or static page data) require a full rebuild.
The pattern that makes preview deploys genuinely useful: pair each preview deployment with an isolated database branch. Neon and Supabase both support branch-per-preview. Each PR gets its own database state. Migrations and seed data apply to the branch, not the production database. The branch is deleted when the PR merges.
Set the database connection string as a branch-scoped env var in Vercel. When the PR deploy runs, it reads the branch-specific connection string and never touches production data.
Vercel caching operates at multiple layers. Understanding which layer holds a cached response determines how to invalidate it, how long it persists, and who benefits from it.
Static files and statically rendered pages. Lives at PoP nodes globally. Served without touching compute. Invalidated on new deploy.
Cached responses with a TTL. After expiry, the next request triggers regeneration in the background. Stale content served during regen.
Per-request or tag-based cache for fetch() calls in server components. Persists across deploys. Invalidated with revalidateTag() or revalidatePath().
Server-rendered output cached at build time for static routes. Reused across requests without re-running server components.
ISR is often described as "regenerates every N seconds." The actual behavior is more precise. When you export revalidate = 60 from a route, Vercel serves the cached response for 60 seconds. After 60 seconds, the next request triggers background regeneration. The requesting user still gets the stale response. The next user after regeneration completes gets the fresh one.
This is stale-while-revalidate. The tradeoff: low latency (cached response always returned immediately) at the cost of up-to-N seconds of stale data. For content that changes frequently and must be current, use dynamic = 'force-dynamic' and accept the function invocation cost on every request.
Next.js <Image> on Vercel uses Vercel's Image Optimization service automatically. Vercel resizes, converts to WebP or AVIF, and caches optimized variants at the edge. Remote image sources must be declared in next.config.ts via remotePatterns.
Plan limits apply: Hobby allows 1,000 source images per month. Pro allows 5,000. Exceeding the limit incurs per-image overage charges. If you process many images, cache generated variants in object storage (Cloudflare R2, S3) and serve them directly, bypassing Vercel's optimizer on repeated requests.
Vercel pricing has several dimensions that are easy to overlook until a bill arrives. The categories that most often surprise teams are function duration, bandwidth, image optimization, and seat count.
Function duration is the primary lever. You are billed per GB-second (memory x duration). A function allocated 1 GB of memory that runs for 10 seconds per invocation costs 10 GB-seconds. At 100,000 invocations per month, that is 1,000,000 GB-seconds. Fluid compute reduces this by sharing instance cost across concurrent requests. A warm instance handling 5 concurrent requests accumulates 5x the invocations but only 1x the duration cost on the running instance.
| Cost Driver | When It Accumulates | Mitigation |
|---|---|---|
| Long-running functions | AI streaming, large file processing | Fluid compute concurrency; streaming response patterns |
| No caching on static routes | force-dynamic on routes that could be ISR |
Use revalidate = N for content that tolerates some staleness |
| Image optimization overages | High-volume image-heavy sites on Hobby | Pre-process images to WebP/AVIF in CDN; serve from R2 |
| Bandwidth overages | Serving large files (PDFs, videos) from functions | Use R2 or S3 presigned URLs; never stream large assets through functions |
| Preview build minutes | Many branches, frequent pushes | Disable preview builds for non-code branches in project settings |
| Database connections | Serverless functions opening new DB connections per invocation | Connection pooler (Neon pooler, PgBouncer) or Fluid compute persistent connections |
Classic serverless creates a new database connection on every cold start and drops it when the instance dies. At scale, hundreds of short-lived connections overwhelm Postgres's connection limit. Neon and Supabase both provide connection poolers to absorb this. Fluid compute changes the equation: instances stay warm across requests and can hold persistent connections. With Fluid compute enabled, you can open a connection in module scope and reuse it across concurrent requests on the same instance, reducing total connection count significantly. Still use a pooler for safety, but the pressure is far lower.
NEXT_PUBLIC_ vars are public. Full stop. The prefix is a signal that the value is safe to ship in the browser bundle. Treat it as such. A token in a NEXT_PUBLIC_ var is exposed to every visitor who opens DevTools.waitUntil is the right place for post-response work. Analytics writes, cache priming, and notifications do not belong inside the response path. Use waitUntil from @vercel/functions to run them after the response is sent without blocking the user.Authorization: Bearer <CRON_SECRET> header in cron handlers. Without this check, any caller with the URL can trigger scheduled jobs on demand.New deep-dives on systems architecture, delivered when they ship.