Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
The server request lifecycle governs how incoming HTTP connections are received, abstracted, processed, and rendered across different runtime environments. It coordinates server initialization, uniform request/response encapsulation, metadata tracking, routing pipelines, and rendering execution for both the App and Pages routers, while providing robust error handling and development-mode compilation hooks.
Sources: packages/next/src/server/base-server.ts:1714-1729, packages/next/src/server/base-http/index.ts:28-103, packages/next/src/server/request-meta.ts:51-338, packages/next/src/server/lib/router-server.ts:371-431
Incoming HTTP connections are received and initialized through startServer or wrapped via NextServer and NextCustomServer classes. The server establishes network listeners, handles port retries and process cleanups, and delegates incoming socket and request events into router handlers.
Sources: packages/next/src/server/lib/start-server.ts:184-295, packages/next/src/server/next.ts:183-248
The startup procedure orchestrates socket binding, worker messaging, and configuration parsing through a deterministic call sequence.
Sources: packages/next/src/server/lib/start-server.ts:139-178, packages/next/src/server/lib/start-server.ts:274-321
Next.js provides distinct server wrappers for production execution (NextServer), custom server integrations via import next from 'next' (NextCustomServer), and worker-based process spawning.
Sources: packages/next/src/server/next.ts:183-612, packages/next/src/server/lib/start-server.ts:184-353
Note
During server initialization in development (isDev), startServer sets up a Watchpack instance monitoring configuration files (CONFIG_FILES) and distribution directories (absDistDir) to trigger an automatic server restart with RESTART_EXIT_CODE upon modifications or deletions.
When an HTTP connection arrives at requestListener, requests await the initialization promise (handlersPromise) before being passed to requestHandler. Upgrade requests are similarly captured and routed through upgradeHandler.
async function requestListener(req: IncomingMessage, res: ServerResponse) {
try {
if (handlersPromise) {
await handlersPromise
handlersPromise = undefined
}
await requestHandler(req, res)
} catch (err) {
res.statusCode = 500
res.end('Internal Server Error')
Log.error(`Failed to handle request for ${req.url}`)
console.error(err)
}
}Next.js unifies request and response handling across Node.js and Web standard runtime environments through abstract base classes (BaseNextRequest and BaseNextResponse) implemented by concrete runtime wrappers (NodeNextRequest, NodeNextResponse, WebNextRequest, and WebNextResponse). This encapsulation allows server pipelines to interact with request streams, headers, cookies, and status codes uniformly regardless of whether execution occurs in a Node.js server environment or a Web-standard edge sandbox.
Sources: packages/next/src/server/base-http/index.ts:28-103, packages/next/src/server/base-http/node.ts:19-169, packages/next/src/server/base-http/web.ts:11-140
The architecture relies on abstract classes that define common properties and shared utility methods like cookie parsing and redirect helpers, while delegating low-level input/output operations to runtime-specific classes.
Sources: packages/next/src/server/base-http/index.ts:28-103, packages/next/src/server/base-http/node.ts:19-169, packages/next/src/server/base-http/web.ts:11-140
Warning
NodeNextRequest.stream() can only be called once. Attempting to consume or convert the Node request body into a Web ReadableStream more than once throws an invariant error because the underlying Node.js stream begins flowing immediately upon attaching data handlers.
In web-standard and edge runtimes, WebNextResponse manages output through a TransformStream and a CloseController. When completing execution, toResponse() resolves pending promises, wraps body consumption if listeners are present, and returns a standard Web Response.
export class WebNextResponse extends BaseNextResponse<WritableStream> {
private headers = new Headers()
private textBody: string | undefined = undefined
private closeController = new CloseController()
public statusCode: number | undefined
public statusMessage: string | undefined
constructor(public transformStream = new TransformStream()) {
super(transformStream.writable)
}
setHeader(name: string, value: string | string[]): this {
this.headers.delete(name)
for (const val of Array.isArray(value) ? value : [value]) {
this.headers.append(name, val)
}
return this
}
}Caution
Calling onClose() on a WebNextResponse instance that is already closed triggers an InvariantError. Lifecycle callbacks must be registered before the response body finishes streaming and the close controller dispatches its close event.
Next.js attaches routing context, parsed parameters, incremental cache references, and body cloning state directly to incoming HTTP request objects using a private symbol key. This avoids polluting public request properties while allowing internal modules to share request-scoped data across boundaries.
export const NEXT_REQUEST_META = Symbol.for('NextInternalRequestMeta')
export type NextIncomingMessage = (
| BaseNextRequest
| IncomingMessage
| NextRequest
) & {
[NEXT_REQUEST_META]?: RequestMeta
}State attachment and retrieval are handled by helper functions that read or mutate the record stored under NEXT_REQUEST_META.
export function getRequestMeta(
req: NextIncomingMessage,
key?: undefined
): RequestMeta
export function getRequestMeta<K extends keyof RequestMeta>(
req: NextIncomingMessage,
key: K
): RequestMeta[K]
export function getRequestMeta<K extends keyof RequestMeta>(
req: NextIncomingMessage,
key?: K
): RequestMeta | RequestMeta[K] {
const meta = req[NEXT_REQUEST_META] || {}
return typeof key === 'string' ? meta[key] : meta
}
export function setRequestMeta(req: NextIncomingMessage, meta: RequestMeta) {
req[NEXT_REQUEST_META] = meta
return meta
}
export function addRequestMeta<K extends keyof RequestMeta>(
request: NextIncomingMessage,
key: K,
value: RequestMeta[K]
) {
const meta = getRequestMeta(request)
meta[key] = value
return setRequestMeta(request, meta)
}
export function removeRequestMeta<K extends keyof RequestMeta>(
request: NextIncomingMessage,
key: K
) {
const meta = getRequestMeta(request)
delete meta[key]
return setRequestMeta(request, meta)
}To prevent headers set by middleware from being overwritten or dropped by downstream API routes or getServerSideProps, Next.js patches the response object's setHeader method.
export function patchSetHeaderWithCookieSupport(
req: NextIncomingMessage,
res: PatchableResponse
) {
if (res[PATCHED_SET_HEADER]) {
return
}
const setHeader = res.setHeader.bind(res)
Object.defineProperty(res, PATCHED_SET_HEADER, {
value: true,
})
res.setHeader = (
name: string,
value: string | string[]
): PatchableResponse => {
if ('headersSent' in res && res.headersSent) {
return res
}
if (name.toLowerCase() === 'set-cookie') {
const middlewareValue = getRequestMeta(req, 'middlewareCookie')
if (
!middlewareValue ||
!Array.isArray(value) ||
!value.every((item, idx) => item === middlewareValue[idx])
) {
value = [
...new Set([
...(middlewareValue || []),
...(typeof value === 'string'
? [value]
: Array.isArray(value)
? value
: []),
]),
]
}
}
return setHeader(name, value)
}
}Note
patchSetHeaderWithCookieSupport uses a symbol flag PATCHED_SET_HEADER to guarantee that the response object is patched at most once per request, avoiding recursive wrapper overhead when multiple handlers invoke setHeader.
NextServer / NextNodeServer)Server classes coordinate the core request handling loop, URL normalization, locale analysis, and routing execution for Next.js applications. Incoming requests flow through normalization routines that filter pathname information, inspect specialized request markers, and execute tracing spans.
Sources: packages/next/src/server/base-server.ts:1637-1661, packages/next/src/server/base-server.ts:1747-1764
Pathnames are normalized using an ordered array of normalizers checked sequentially. If a normalizer matches the pathname, its normalization logic is applied directly.
private normalize = (pathname: string) => {
const normalizers: Array<PathnameNormalizer> = []
if (this.normalizers.data) {
normalizers.push(this.normalizers.data)
}
// We have to put the segment prefetch normalizer before the RSC normalizer
// because the RSC normalizer will match the prefetch RSC routes too.
if (this.normalizers.segmentPrefetchRSC) {
normalizers.push(this.normalizers.segmentPrefetchRSC)
}
if (this.normalizers.rsc) {
normalizers.push(this.normalizers.rsc)
}
for (const normalizer of normalizers) {
if (!normalizer.match(pathname)) continue
return normalizer.normalize(pathname, true)
}
return pathname
}Caution
The segment prefetch normalizer must be evaluated before the React Server Components (RSC) normalizer in the array. Because the RSC normalizer matches prefetch RSC routes as well, placing it first would intercept and misroute segment prefetches.
The request handling pipeline coordinates tracing spans, streaming context construction, and execution routing:
run() / runImpl(): Wraps execution within tracing spans and dispatches the catch-all render request handler.pipe() / pipeImpl(): Inspects the user-agent header, creates a cloned RequestContext with supportsDynamicResponse based on bot detection status, and determines whether streaming metadata should be served.normalizeAndAttachMetadata(): Evaluates specialized request handlers like image optimization and pages data endpoints before standard routing.Sources: packages/next/src/server/base-server.ts:1663-1676, packages/next/src/server/base-server.ts:1747-1763, packages/next/src/server/base-server.ts:1765-1804
Sources: packages/next/src/server/base-server.ts:1637-1661, packages/next/src/server/base-server.ts:1714-1728, packages/next/src/server/base-server.ts:1751-1754
NextNodeServer orchestrates concrete request routing and dispatch for Pages API routes, internal error pages, and image optimization pipelines. When an incoming request targets an API endpoint, handleApiRequest delegates execution directly to runApi. Similarly, internal rendering errors or specialized fallback routes trigger specific handler branches like renderErrorToResponseImpl, which evaluates edge function availability for special entries such as UNDERSCORE_NOT_FOUND_ROUTE_ENTRY.
Sources: packages/next/src/server/next-server.ts:1232-1239, packages/next/src/server/next-server.ts:1356-1384
The image optimization subsystem processes requests via ImageOptimizerCache and imageOptimizer. It validates parameters, manages LRU disk caches or custom cache handlers, and executes image transformations.
export async function imageOptimizer(
imageUpstream: ImageUpstream,
paramsResult: Pick<ImageParamsResult, 'href' | 'width' | 'quality' | 'mimeType'>,
nextConfig: { ... },
opts: { isDev?: boolean; silent?: boolean; previousCacheEntry?: IncrementalResponseCacheEntry | null }
) {
const { href, quality, width, mimeType } = paramsResult
const { buffer: upstreamBuffer, etag: upstreamEtag } = imageUpstream
const maxAge = Math.max(nextConfig.images.minimumCacheTTL, getMaxAge(imageUpstream.cacheControl))
// Detects content type, validates SVG policies, handles animated/bypass types, and optimizes buffer
}The image request processing pipeline moves through specific validation, fetching, and transformation stages:
ImageOptimizerCache.validateParams(): Inspects query parameters url, w, and q, verifying domain whitelist rules, local patterns, and numerical size constraints.fetchExternalImage() / fetchInternalImage(): Fetches the upstream resource while checking IP restrictions, response body size limits, and redirect thresholds.imageOptimizer(): Inspects magic numbers via detectContentType(), checks SVG permissions, bypasses animated or raw image types, and runs optimizeImage() using Sharp.sendResponse(): Sets response headers such as Cache-Control, Vary: Accept, Content-Disposition, and X-Nextjs-Cache before streaming the optimized buffer.Sources: packages/next/src/server/image-optimizer.ts:387-549, packages/next/src/server/image-optimizer.ts:872-1054, packages/next/src/server/image-optimizer.ts:1056-1236, packages/next/src/server/image-optimizer.ts:1292-1327
Warning
Requesting an external image whose hostname resolves to a private IP address will trigger a 400 error and throw an ImageError, unless images.dangerouslyAllowLocalIP is explicitly enabled in configuration.
The rendering dispatcher coordinates payload generation across both the Pages router (via React Fizz/SSR and renderToHTMLImpl) and the App Router (via React Flight streaming and server component rendering trees). The server pipeline constructs execution contexts, evaluates headers such as NEXT_ROUTER_PREFETCH_HEADER and RSC_HEADER, and delegates execution down to specific module renderers.
Sources: packages/next/src/server/base-server.ts:1765-1804, packages/next/src/server/app-render/app-render.tsx:395-470, packages/next/src/server/render.tsx:459-468
When a rendering request hits the server, parseRequestHeaders() inspects incoming HTTP headers to establish the exact rendering intent, distinguishing between standard RSC navigation, prefetch variants, and HMR refreshes.
function parseRequestHeaders(
headers: IncomingHttpHeaders,
options: ParseRequestHeadersOptions
): ParsedRequestHeaders {
const isPrefetchRequest = headers[NEXT_ROUTER_PREFETCH_HEADER] === '1'
const isAppShellPrefetchRequest = headers[NEXT_ROUTER_PREFETCH_HEADER] === '3'
const isRuntimePrefetchRequest =
headers[NEXT_ROUTER_PREFETCH_HEADER] === '2' || isAppShellPrefetchRequest
const isHmrRefresh = headers[NEXT_HMR_REFRESH_HEADER] !== undefined
const isRSCRequest = isRSCRequestHeader(headers[RSC_HEADER])
// ...The pipe() and pipeImpl() methods manage request context construction, appending bot detection parameters and streaming metadata flags before executing the render delegate.
private async pipeImpl(
fn: (
ctx: RequestContext<ServerRequest, ServerResponse>
) => Promise<ResponsePayload | null>,
partialContext: Omit<
RequestContext<ServerRequest, ServerResponse>,
'renderOpts'
>
): Promise<void> {
const ua = partialContext.req.headers['user-agent'] || ''
const ctx: RequestContext<ServerRequest, ServerResponse> = {
...partialContext,
renderOpts: {
...this.renderOpts,
supportsDynamicResponse: !this.renderOpts.botType,
serveStreamingMetadata: shouldServeStreamingMetadata(
ua,
this.nextConfig.htmlLimitedBots
),
},
}
const payload = await fn(ctx)Sources: packages/next/src/server/app-render/app-render.tsx:482-512, packages/next/src/server/render.tsx:459-468
Note
App Shell prefetches (where NEXT_ROUTER_PREFETCH_HEADER is set to '3') omit dynamic parameter resolution during server rendering. Any attempt to await params inside an App Shell prefetch will hang indefinitely, producing a param-independent shell of the route.
Sources: packages/next/src/server/app-render/app-render.tsx:380-385, packages/next/src/server/app-render/app-render.tsx:403-404
Development-mode execution in Next.js wraps standard request routing with performance instrumentation, memory tracking, on-demand compilation triggers, and enhanced error stack formatting. Development server implementations override core request handlers to ensure bundler service integration and error overlays operate transparently during local development.
Sources: packages/next/src/server/dev/next-dev-server.ts:570-591, packages/next/src/server/dev/next-dev-server.ts:624-642
Incoming requests in the development server pass through handleRequest(), which initiates a performance trace span (handle-request), waits for server readiness via this.ready?.promise, and attaches debug flags such as PagesErrorDebug. Upon completion, RSS and heap metrics are captured via process.memoryUsage() and logged as child telemetry spans.
public async handleRequest(
req: NodeNextRequest,
res: NodeNextResponse,
parsedUrl?: NextUrlWithParsedQuery
): Promise<void> {
const span = trace('handle-request', undefined, { url: req.url })
const result = await span.traceAsyncFn(async () => {
await this.ready?.promise
addRequestMeta(req, 'PagesErrorDebug', this.renderOpts.ErrorDebug)
return await super.handleRequest(req, res, parsedUrl)
})
const memoryUsage = process.memoryUsage()
span
.traceChild('memory-usage', {
url: req.url,
'memory.rss': String(memoryUsage.rss),
'memory.heapUsed': String(memoryUsage.heapUsed),
'memory.heapTotal': String(memoryUsage.heapTotal),
})
.stop()
return result
}Warning
Unhandled errors thrown during run() in development servers are passed through getProperError(), formatted via formatServerError(), and dispatched to logErrorWithOriginalStack() before triggering renderError(). If headers have already been sent, the response status is forced to 500 with an emergency text fallback.
The development server manages module compilation and telemetry initialization through lifecycle hooks. loadInstrumentationModule() checks for the presence of an instrumentation hook file, ensures it is compiled via ensurePage(), and retrieves the module via getInstrumentationModule().
protected async loadInstrumentationModule(): Promise<any> {
let instrumentationModule: any
if (
this.actualInstrumentationHookFile &&
(await this.ensurePage({
page: this.actualInstrumentationHookFile!,
clientOnly: false,
definition: undefined,
})
.then(() => true)
.catch(() => false))
) {
try {
instrumentationModule = await getInstrumentationModule(
this.dir,
this.nextConfig.distDir
)
} catch (err: any) {
err.message = `An error occurred while loading instrumentation hook: ${err.message}`
throw err
}
}
return instrumentationModule
}Sources: packages/next/src/server/dev/next-dev-server.ts:570-591, packages/next/src/server/dev/next-dev-server.ts:644-649, packages/next/src/server/dev/next-dev-server.ts:714-759