Deployment, Runtime Selection, Scaling, and Rollback

Deployment, runtime selection, scaling, and rollback in Next.js is the discipline of deciding where each route executes, how new builds reach users, how capacity grows under load, and how you return to a known-good version when a release misbehaves. The outcome is not simply a successful next build. The outcome is a deployable artifact, routes assigned to runtimes that match their dependencies, scaling behavior that protects latency and downstream systems, and a rollback path that has already been tested.

This chapter sits in the operations part of the Next.js course because rendering choices made earlier become infrastructure behavior at release time. A static product page, a Server Component that fetches cached data, an Edge middleware check, and a Node.js route handler all ship differently and fail differently. You will learn to read those differences before choosing a host, a runtime, or a rollout strategy.

How a Next.js Deployment Is Built

A production deployment starts with next build. The build analyzes the App Router tree, compiles server and client modules, creates client chunks for files below "use client" boundaries, emits server bundles for Server Components and route handlers, and records route metadata in the .next directory. Static routes can be rendered to files. Dynamic routes become server entries that the platform invokes per request or revalidates according to cache policy.

The important internal detail is that a Next.js application is not one uniform server program. It is a set of route segments with metadata. A segment can force dynamic rendering, opt into a runtime, define revalidation, or inherit settings from parent layouts. During deployment, the platform maps those entries to static assets, Node.js functions, Edge functions, image optimization endpoints, and cache records. If you understand that mapping, deployment decisions become much less mysterious.

Deployment artifact What it serves Operational concern
static asset HTML, CSS, JS chunks, fonts, and images that do not require request-time rendering cache headers and invalidation
Node.js function route handlers and dynamic rendering that need Node APIs or long-lived libraries cold starts, memory, connections, and regional placement
Edge function small request-time logic near users limited APIs, bundle size, CPU budget, and unsupported Node packages
standalone server a self-hosted output containing the minimal production server files container lifecycle, health checks, and process management

Runtime Selection Anatomy

In the App Router, runtime selection is usually expressed with exported segment config. The most common switch is export const runtime = "edge" or export const runtime = "nodejs". You can also shape rendering with dynamic, revalidate, and fetch cache options. These exports are read at build time, so they need to be top-level constants, not values computed from a database or environment-specific request.

Choose nodejs when the route needs Node APIs, database drivers that open TCP sockets, filesystem access available in your deployment, native modules, large dependencies, or longer work. Choose edge for compact request logic such as redirects, A/B assignment, geolocation-aware routing, or authorization gates that use Web APIs. Edge code should be treated as latency-sensitive request code, not as a place for heavy business processing.

export const runtime = "edge";

export async function GET(request) {
  const country = request.headers.get("x-vercel-ip-country") || "unknown";
  return Response.json({ runtime: "edge", country });
}

This first example is intentionally small. The route returns JSON using the standard Response API, which is available in the Edge runtime. A request without the platform header deterministically returns {"runtime":"edge","country":"unknown"}. If you imported fs here, the build would fail or the route would be rejected because the Edge runtime does not provide Node’s filesystem module.

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function POST(request) {
  const body = await request.json();
  if (typeof body.email !== "string") {
    return Response.json({ error: "email is required" }, { status: 400 });
  }
  return Response.json({ queued: true, email: body.email.toLowerCase() });
}

The second example moves to Node.js and forces request-time execution. This is a better shape for work that may call a database, queue, or payment SDK. The deterministic failure behavior is a 400 response when email is missing or not a string. In a real application, the queue write should be idempotent, because platforms may retry failed invocations and users may resubmit forms.

export const revalidate = 300;

async function getCatalog() {
  return [{ sku: "course-nextjs", name: "Next.js Full-Stack Development" }];
}

export default async function CatalogPage() {
  const products = await getCatalog();
  return products.map((product) => product.name).join(", ");
}

The third example shows incremental caching. The route can be served from cached output for up to five minutes, then regenerated. Users get fast responses, but they may see old catalog data briefly. That trade-off is acceptable for many catalog pages and dangerous for inventory reservations, account balances, or permission decisions.

Scaling Mechanics

Next.js scaling depends on the artifact type. Static assets scale through a CDN. Cached Server Component output scales through cache hits. Dynamic Node routes scale by creating more function or server instances. Edge routes scale by running short functions across many nearby locations. Self-hosted deployments scale by adding containers or processes behind a load balancer.

The hidden bottleneck is often not Next.js itself. A route that opens a new database connection per invocation can exhaust the database while the application platform appears healthy. A Server Component that fetches uncached data from three APIs can multiply latency during traffic spikes. Image optimization can saturate CPU if unbounded user images are transformed on demand. Scaling therefore means controlling concurrency, caching, connection pooling, timeouts, and backpressure together.

Use static rendering and revalidate for public data that tolerates staleness. Use dynamic rendering for user-specific data, secrets, and rapidly changing values. Put small, deterministic request decisions at the Edge only when the required libraries fit that environment. Keep long-running jobs outside request handlers by sending work to a queue and returning a clear accepted response.

Deployment Targets and Configuration

A managed Next.js platform usually understands the .next build output and deploys each artifact to the appropriate service. Self-hosting commonly uses output: "standalone", which copies the minimal server and production dependencies needed to run server.js. Standalone output does not remove the need for environment variables, migrations, image storage, health checks, log routing, or rollback automation.

const nextConfig = {
  output: "standalone",
  poweredByHeader: false,
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "images.example.com" }
    ]
  }
};

module.exports = nextConfig;

This configuration prepares a self-hosted build and restricts optimized remote images to a known host. The expected build result is a .next/standalone directory after next build. The image setting matters operationally: without a narrow allow list, an attacker may be able to make your server fetch and transform unexpected remote content.

Rollback Design

A rollback is a release operation, not an apology after the fact. The best rollback unit is an immutable build identified by a commit SHA, image digest, or platform deployment id. Configuration changes and database migrations complicate rollback because application code may move backward while schema or environment state stays forward. For that reason, risky releases should use backward-compatible migrations: expand the schema, deploy code that can read both shapes, backfill, then remove the old shape later.

For managed deployments, rollback usually means promoting a previous deployment. For containers, it means redeploying a previous image digest or changing the orchestrator revision. For static assets, it also means making sure cache invalidation does not leave browsers with mismatched HTML and JavaScript chunks. A practical rollback runbook names the version to restore, the command or console action, the health checks to watch, and the data changes that must not be reversed automatically.

Failure Modes and Troubleshooting

Build fails after setting Edge runtime. The symptom is an error about unsupported modules such as fs, net, native bindings, or a database driver. The cause is that Edge functions run in a Web API environment. Diagnose by reading the import trace in the build output and checking transitive dependencies. Correct it by moving the route to nodejs, replacing the library with a Web-compatible client, or moving the heavy work behind a Node route.

Production works locally but times out under load. The symptom is a rising p95 latency, intermittent 504 responses, or database connection errors. The usual cause is dynamic rendering that fans out to slow services or opens too many connections as functions scale. Diagnose by logging route name, runtime, dependency latency, and connection errors without logging secrets. Correct it with caching, pooled or proxy-based database access, shorter timeouts, queueing, and a concurrency limit around the slow dependency.

Rollback does not restore the old behavior. The symptom is that the previous build is active but users still see errors. Common causes include incompatible migrations, stale CDN assets, changed environment variables, or external data written by the bad release. Diagnose by comparing deployment id, environment version, schema version, and asset headers. Correct it by restoring compatible configuration, serving a build whose assets match its HTML, and applying a forward fix for data that cannot be safely rolled back.

Security, Performance, and Reliability Implications

Runtime selection changes the security surface. Edge routes should receive only the secrets they need, and many teams avoid putting high-value service credentials there because the code is distributed broadly. Node routes that handle mutations must authenticate, authorize, validate input, and make side effects idempotent. Static and cached pages must never embed user-specific secrets into shared output.

Performance comes from putting work in the cheapest correct place. CDN assets beat server rendering. Cached Server Component output beats repeated upstream calls. Edge checks can reduce round trips but can also slow every request if they import too much code. Reliability comes from release separation: code deployment, configuration rollout, and schema migration should be independently observable and reversible where possible.

Hands-On Lab: Runtime and Rollback Drill

Prerequisites: a Next.js App Router project, Node.js installed, package scripts for dev and build, and a Git branch you can reset or discard. The lab creates two route handlers and a self-hosting configuration, then verifies runtime-specific behavior.

  1. Create app/api/where/route.js with the Edge example from this lesson. Run the development server and request /api/where. Verify that the JSON includes "runtime":"edge".
  2. Create app/api/signup/route.js with the Node.js POST example. Send {} and verify a 400 response. Send {"email":"A@EXAMPLE.COM"} and verify {"queued":true,"email":"a@example.com"}.
  3. Add the standalone next.config.js fragment if your project does not already have conflicting config. Run npm run build. Verify that the build completes and, for standalone output, that .next/standalone exists.
  4. Commit the working state or record the commit SHA. Then intentionally break the Edge route by importing fs. Run the build again and observe the runtime compatibility failure.
  5. Rollback by reverting the bad change or checking out the recorded SHA. Run the build and the two route checks again. Verification succeeds only when both routes return the expected responses and the build is clean.

Cleanup: remove the lab routes if they were only for practice, undo any temporary config change, and delete build output if your repository does not track it. Keep the rollback notes; they are the useful artifact.

Assessment Exercises

  1. A dashboard route uses a database driver, reads per-user rows, and must show fresh data. Which runtime and rendering mode would you choose, and what scaling bottleneck would you test first?
  2. An Edge middleware imports a shared analytics package and the build starts failing. Explain how you would identify the incompatible import and redesign the boundary.
  3. A product page changes twice per hour and receives heavy anonymous traffic. Design a caching and revalidation policy, then state what stale behavior users may observe.
  4. A release includes a database column rename and a UI change. Describe a rollback-safe migration sequence that avoids breaking the old build.
  5. Your platform auto-scales functions, but the database reports too many connections. Name two application changes and one infrastructure change that could reduce the failure rate.

Summary

Next.js deployment is the process of turning a route tree into static assets, server entries, cache records, and runtime-specific functions. Runtime selection decides whether code runs in a constrained Edge environment or a fuller Node.js environment. Scaling improves when public work is cached, request-time work is bounded, and downstream dependencies are protected. Rollback works when builds are immutable, migrations are compatible, and the team has verified the exact steps before a bad release forces the issue.