Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

Trace Profiling

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Next.js incorporates robust tracing and performance profiling subsystems designed to capture, record, and export telemetry data across both compilation pipelines and runtime request handlers. By leveraging standard OpenTelemetry instrumentation alongside custom local span recording mechanisms, the tracing engine provides granular visibility into execution paths, network request waterfalls, and resource utilization. Sources: packages/next/src/server/lib/trace/tracer.ts:223-278, packages/next/src/server/lib/trace/local-span-recorder.ts:58-81

The system addresses the challenge of diagnosing performance bottlenecks in complex asynchronous environments by combining asynchronous local storage propagation, bundler compilation hooks for Webpack and Rspack, output file dependency tracing via Vercel's NFT, and periodic V8 memory heap tracking. Sources: packages/next/src/build/webpack/plugins/profiling-plugin.ts:28-47, packages/next/src/build/collect-build-traces.ts:94-126, packages/next/src/lib/memory/trace.ts:27-54

Tracer Architecture and Core Span Lifecycle

Public Tracer Interface and Active Scope Management

Next.js exports a unified tracing surface combining OpenTelemetry wrappers with custom span management primitives. The NextTracerImpl class implements active scope resolution and context propagation using @opentelemetry/api. Active scope tracking through getActiveScopeSpan() inspects local span recorders when running in development mode before falling back to trace.getSpan(context.active()).

Note

NextTracerImpl attempts to require the user's installed version of @opentelemetry/api first to maintain compatibility with external OpenTelemetry tracing setups, falling back to the version bundled with Next.js only if the require call fails.

Context propagation is managed via withPropagatedContext and runWithDetachedContext. When executing synchronous or asynchronous blocks where upstream trace headers must be honored, withPropagatedContext extracts remote context carriers via propagation.extract using a provided or default TextMapGetter. If force is enabled, any extracted remote span context overrides the active context while merging baggage values. Conversely, runWithDetachedContext executes functions under ROOT_CONTEXT, ensuring that in-process Node.js middleware spawns sibling spans rather than nested child spans, matching the detached sandbox behavior of edge middleware.

Sources: packages/next/src/server/lib/trace/tracer.ts:42-50, packages/next/src/server/lib/trace/tracer.ts:252-330

OpenTelemetry Span Lifecycle Hooks

Span creation and duration recording are managed by the Span class in packages/next/src/trace/trace.ts alongside predefined span type enumerations. When a span starts, it captures a high-resolution timestamp via process.hrtime.bigint() and a wall-clock fallback via Date.now() to correlate monotonic durations with absolute epoch times.

Sources: packages/next/src/trace/trace.ts:30-164

The Span lifecycle supports manual and automatic tracking. Durations are calculated as nanosecond differences scaled down to microfluidic integer microseconds to fit safely within float64 bounds (Number.MAX_SAFE_INTEGER). If the calculated duration exceeds RECORD_SPAN_THRESHOLD_MS * 1000, the resulting TraceEvent is dispatched to the global reporter.

Span EnumerationDescription / Key MembersSources
BaseServerSpanRequest handling, pipeline execution, server-side rendering (handleRequest, render, renderToHTML)packages/next/src/server/lib/trace/constants.ts
NextNodeServerSpanNode server internals, component tree creation, routing, static generation (createComponentTree, findPageComponents, runApi)packages/next/src/server/lib/trace/constants.ts
AppRenderSpanApp Router rendering phases (renderToString, renderToReadableStream, fetch, waitShellReady)packages/next/src/server/lib/trace/constants.ts
RenderSpanPages Router data fetching and rendering (getServerSideProps, getStaticProps, renderDocument)packages/next/src/server/lib/trace/constants.ts
LoadComponentsComponent and route module loading (loadComponents, loadRouteModule)packages/next/src/server/lib/trace/constants.ts
MiddlewareSpanMiddleware execution tracing (execute)packages/next/src/server/lib/trace/constants.ts

Caution

Calling .stop() multiple times on the same Span instance is guarded by a status check against SpanStatus.Stopped, returning early without reporting duplicate trace events. However, callers must ensure asynchronous functions wrapped with traceAsyncFn() properly await completion so that finally blocks call stop() at the correct boundary.

Sources: packages/next/src/server/lib/trace/constants.ts:10-134, packages/next/src/trace/trace.ts:75-103, packages/next/src/trace/trace.ts:154-164

Local Span Recording and Storage

In-Memory Collection and Async Storage Propagation

Local span recording captures telemetry directly within the development server environment without exporting to external OpenTelemetry collectors. This subsystem relies on AsyncLocalStorage instances managed across two storage boundaries: localSpanAsyncStorage via packages/next/src/server/lib/trace/local-span-recorder.ts and request-insights-identity-storage via packages/next/src/server/lib/trace/request-insights-identity.ts.

Sources: packages/next/src/server/lib/trace/local-span-recorder.ts:83-128, packages/next/src/server/lib/trace/request-insights-identity.ts:15-30

The function createLocalSpan instantiates LocalRecordingSpan with hex-encoded trace IDs and span IDs generated incrementally via getLocalTraceId() (32 hex characters) and getLocalSpanId() (16 hex characters). When traceLocalSpan wraps asynchronous operations, it automatically extracts the active local span's context, propagates parent identifiers, and isolates the span from the global OpenTelemetry pipeline by setting isolateOpenTelemetry: true.

Note

Request insights identity storage covers the portion of BaseServer request handling executing before app rendering establishes work storage. Once workStore is available, it functions as the primary identity source for locally recorded spans.

Sources: packages/next/src/server/lib/trace/local-span-recorder.ts:48-128, packages/next/src/server/lib/trace/request-insights-identity.ts:12-17

DevTools Integration and Recorder Registration

Span persistence and developer tooling integration are controlled by environment checks and registration functions defined in packages/next/src/server/lib/trace/span-store.ts. Local span recording is active only when process.env.__NEXT_DEV_SERVER is enabled alongside active test recorders or request insights flags.

Function / ConstantScope / Return TypeDescriptionSources
isLocalSpanRecordingEnabled()booleanChecks if __NEXT_DEV_SERVER is present and either test recorders or request insights are enabled.packages/next/src/server/lib/trace/span-store.ts
isRequestInsightsEnabled()booleanValidates if process.env.__NEXT_REQUEST_INSIGHTS matches enabled values (1, true, or boolean true).packages/next/src/server/lib/trace/span-store.ts
recordSpan(record)voidAttaches a timestamp to the span record and dispatches it to test recorders or request insight collectors.packages/next/src/server/lib/trace/span-store.ts
registerLocalSpanRecorder()voidAttaches a LocalSpanRecorder dictionary to globalThis under the symbol @next/local-span-recorder.packages/next/src/server/lib/trace/local-span-recorder.ts

Caution

recordSpan drops incoming records immediately without processing if isLocalSpanRecordingEnabled() evaluates to false, ensuring zero overhead during production builds where development server flags are absent.

Sources: packages/next/src/server/lib/trace/span-store.ts:45-76

Instrumentation Initialization and Node Augmentations

Instrumentation Hook Loading and Registration

Server startup initialization requires locating and loading the user-defined instrumentation hook file (instrumentation.js or instrumentation.ts compiled into the distribution directory). The module loader function getInstrumentationModule(projectDir, distDir) caches the resolved instrumentation object to prevent duplicate disk reads, using performance.timeOrigin + performance.performance.now() via getInstrumentationTimestamp() to measure module load timings.

export async function getInstrumentationModule(
  projectDir: string,
  distDir: string
): Promise<InstrumentationModule | undefined> {
  if (cachedInstrumentationModule) {
    return cachedInstrumentationModule
  }
 
  const startTime = getInstrumentationTimestamp()
  let error: unknown
  try {
    cachedInstrumentationModule = interopDefault(
      await require(
        path.join(
          projectDir,
          distDir,
          'server',
          `${INSTRUMENTATION_HOOK_FILENAME}.js`
        )
      )
    )
    return cachedInstrumentationModule
  } catch (err: unknown) {
    if (
      isError(err) &&
      err.code !== 'ENOENT' &&
      err.code !== 'MODULE_NOT_FOUND' &&
      err.code !== 'ERR_MODULE_NOT_FOUND'
    ) {
      error = err
      throw err
    }
  } finally {
    instrumentationModuleLoadTiming ??= {
      startTime,
      endTime: getInstrumentationTimestamp(),
      error,
    }
  }
}

Sources: packages/next/src/server/lib/router-utils/instrumentation-globals.external.ts:62-101

When registerInstrumentation(projectDir, distDir) executes, it checks process.env.NEXT_PHASE === 'phase-production-build' to bypass registration during production compilation. If the instrumentation module exports a register() asynchronous function, it executes the lifecycle hook, triggers extendInstrumentationAfterRegistration(), and emits backdated spans for both module loading and registration phases.

async function registerInstrumentation(projectDir: string, distDir: string) {
  // Ensure registerInstrumentation is not called in production build
  if (process.env.NEXT_PHASE === 'phase-production-build') {
    return
  }
  if (!instrumentationModulePromise) {
    instrumentationModulePromise = getInstrumentationModule(projectDir, distDir)
  }
  let instrumentation: InstrumentationModule | undefined
  try {
    instrumentation = await instrumentationModulePromise
  } catch (err) {
    // A load error is only exportable when a provider was already installed.
    traceInstrumentationModuleLoad()
    throw err
  }
  if (instrumentation?.register) {
    const startTime = getInstrumentationTimestamp()
    let error: unknown
    try {
      await instrumentation.register()
      extendInstrumentationAfterRegistration()
    } catch (err: any) {
      error = err
      err.message = `An error occurred while loading instrumentation hook: ${err.message}`
      throw err
    } finally {
      const endTime = getInstrumentationTimestamp()
 
      // register() commonly installs the provider, so emit both lifecycle
      // spans afterward with their original timestamps.
      traceInstrumentationModuleLoad()
      traceBackdatedInstrumentationSpan(
        InstrumentationSpan.register,
        'register instrumentation',
        { startTime, endTime, error }
      )
    }
  } else {
    traceInstrumentationModuleLoad()
  }
}

Sources: packages/next/src/server/lib/router-utils/instrumentation-globals.external.ts:105-146

Warning

getInstrumentationModule ignores missing hook files by catching ENOENT, MODULE_NOT_FOUND, and ERR_MODULE_NOT_FOUND error codes silently, but rethrows any syntax or runtime errors encountered inside a present instrumentation file.

Sources: packages/next/src/server/lib/router-utils/instrumentation-globals.external.ts:84-93

Tracer Provider and Node Extensions

After successful registration, Node.js environment extensions modify OpenTelemetry tracer behaviors to integrate with React Server Components work units and cache components. The afterRegistration() function verifies that process.env.NEXT_RUNTIME !== 'edge' before invoking extendTracerProviderForCacheComponents().

Sources: packages/next/src/server/lib/router-utils/instrumentation-node-extensions.ts:18-26

Function / ComponentTarget ScopeDescriptionSources
afterRegistration()Node.js RuntimeValidates runtime is not edge and triggers tracer provider extensions.packages/next/src/server/lib/router-utils/instrumentation-node-extensions.ts
isProxyTracerProvider()TracerProviderType guard checking for the presence of getDelegateTracer.packages/next/src/server/lib/router-utils/instrumentation-node-extensions.ts
instrumentTracerForCacheComponents()TracerPatches startSpan and startActiveSpan to exit workUnitAsyncStorage during span creation.packages/next/src/server/lib/router-utils/instrumentation-node-extensions.ts
ensureInstrumentationRegistered()Promise<void>Memoizes registerInstrumentation to prevent concurrent multi-registration attempts.packages/next/src/server/lib/router-utils/instrumentation-globals.external.ts

Tip

The tracer extension attempts to load user-supplied @opentelemetry/api packages first, falling back to Next.js's bundled dependency version if the external module resolution fails.

Sources: packages/next/src/server/lib/router-utils/instrumentation-node-extensions.ts:52-57

Bundler Profiling and Tap Tracing

Webpack and Rspack Compilation Hooks

The ProfilingPlugin and RspackProfilingPlugin capture build metrics by attaching performance measurement spans to compiler tap hooks. Both plugins register lifecycle listeners on compilation phases to trace module construction, chunk generation, asset emission, and loader execution.

apply(compiler: any) {
  this.traceTopLevelHooks(compiler)
  this.traceCompilationHooks(compiler)
  this.compiler = compiler
}
 
traceHookPair(
  spanName: string | (() => string),
  startHook: any,
  stopHook: any,
  {
    parentSpan,
    attrs,
    onStart,
    onStop,
  }: {
    parentSpan?: (...params: any[]) => Span
    attrs?: any
    onStart?: (span: Span, ...params: any[]) => void
    onStop?: (span: Span, ...params: any[]) => void
  } = {}
) {
  let span: Span | undefined
  startHook.tap(
    { name: pluginName, stage: -Infinity },
    (...params: any[]) => {
      const name = typeof spanName === 'function' ? spanName() : spanName
      const attributes = attrs ? attrs(...params) : attrs
      span = parentSpan
        ? parentSpan(...params).traceChild(name, attributes)
        : this.runWebpackSpan.traceChild(name, attributes)
 
      if (onStart) onStart(span, ...params)
    }
  )
  stopHook.tap({ name: pluginName, stage: Infinity }, (...params: any[]) => {
    // `stopHook` may be triggered when `startHook` has not in cases
    // where `stopHook` is used as the terminating event for more
    // than one pair of hooks.
    if (!span) {
      return
    }
 
    if (onStop) onStop(span, ...params)
    span.stop()
  })
}

Sources: packages/next/src/build/webpack/plugins/profiling-plugin.ts:43-89

Plugin NameTarget CompilerKey Lifecycle HooksStored Maps / WeakMapsSources
ProfilingPluginWebpackcompilation, afterCompile, invalid, done, emit, afterEmit, make, buildModule, succeedModule, failedModulespans, moduleSpansByCompilation, makeSpanByCompilation, sealSpanByCompilation, webpackInvalidSpanspackages/next/src/build/webpack/plugins/profiling-plugin.ts
RspackProfilingPluginRspackthisCompilation, loader, buildModule, succeedModulemoduleSpansByCompilation, compilationSpanspackages/next/src/build/webpack/plugins/rspack-profiling-plugin.ts

Warning

ProfilingPlugin registers startHook at -Infinity stage priority and stopHook at Infinity stage priority to ensure compilation boundaries encapsulate all intermediate plugin actions accurately.

Sources: packages/next/src/build/webpack/plugins/profiling-plugin.ts:66-78

Loader Span Profiling and Asset Traces

During module loading, both profiling plugins intercept the normal module loader hook to bind the active module span onto the Webpack or Rspack loaderContext under currentTraceSpan. This allows individual loaders to record custom child measurements inside the active module's lifetime.

moduleHooks.loader.tap(
  pluginName,
  (loaderContext: any, module: any) => {
    const moduleSpan = moduleSpansByCompilation
      .get(compilation)
      ?.get(module)
    loaderContext.currentTraceSpan = moduleSpan
  }
)
 
compilation.hooks.succeedModule.tap(pluginName, (module: any) => {
  moduleSpansByCompilation?.get(compilation)?.get(module)?.stop()
})

Sources: packages/next/src/build/webpack/plugins/profiling-plugin.ts:214-226

moduleHooks.loader.tap(
  pluginName,
  (loaderContext: any, module: any) => {
    const moduleSpan = moduleSpansByCompilation
      .get(compilation)
      ?.get(module)
    loaderContext.currentTraceSpan = moduleSpan
  }
)

Sources: packages/next/src/build/webpack/plugins/rspack-profiling-plugin.ts:35-43

const TRACE_LABELS_SEAL = [
  'module assets',
  'create chunk assets',
  'asset render',
  'asset emit',
  'store asset',
]

Sources: packages/next/src/build/webpack/plugins/profiling-plugin.ts:16-22

Trace Export and Telemetry Reporting

Telemetry Reporting and Event Access Lists

Next.js exports specific internal trace events to anonymous telemetry via reportToTelemetry, which inspects a TRACE_EVENT_ACCESSLIST mapping. When a recorded trace event name matches an entry in this access list and telemetry storage is active via traceGlobals, it records the event payload with its duration in microseconds.

| Trace Event Name | Telemetry Event Identifier | Sources | | --- | --- | --- | --- | | webpack-invalidated | WEBPACK_INVALIDATED | packages/next/src/trace/report/to-telemetry.ts |

Sources: packages/next/src/trace/report/to-telemetry.ts:1-27

Trace Uploader and Allowed Event Filters

The trace uploader processes session trace output files line-by-line, filtering events against predefined allowed sets depending on whether execution is in development or build mode. Root spans and feature flags prefixed with feature. are gathered to populate request metadata, including git branch, commit hash, environment detection flags, and system architecture parameters.

Mode / Set ConstantAllowed Event Names / PrefixesSources
COMMON_ALLOWED_EVENTSmemory-usage, turbopack-persistence, turbopack-compaction, turbopack-module-graph, turbopack-write-entrypoints, turbopack-emitpackages/next/src/trace/trace-uploader.ts
DEV_ALLOWED_EVENTSclient-hmr-latency, render-path, hot-reloader, webpack-invalid-client, webpack-invalidated-server, navigation-to-hydration, start-dev-server, compile-path, memory-usage, server-restart-close-to-memory-threshold plus common eventspackages/next/src/trace/trace-uploader.ts
BUILD_ALLOWED_EVENTSnext-build, run-turbopack, webpack-compilation, run-webpack-compiler, create-entrypoints, worker-main-edge-server, worker-main-client, worker-main-server, server, make, seal, chunk-graph, optimize-modules, optimize-chunks, optimize, optimize-tree, optimize-chunk-modules, module-hash, client, static-check, node-file-trace-build, static-generation, next-export, run-typescript, run-eslint plus common eventspackages/next/src/trace/trace-uploader.ts

Warning

Setting NEXT_TRACE_UPLOAD_FULL bypasses event name filtering to upload the entire trace file, which may fail if the build generated an excessively large trace payload.

Sources: packages/next/src/trace/trace-uploader.ts:65-75

Turbopack Trace Server and MCP Integration

The CLI utility startTurboTraceServerCli loads native SWC bindings and starts a background trace server handle alongside a Model Context Protocol (MCP) server instance on designated ports (wsPort default 5747, mcpPort defaulting to wsPort + 1). The MCP server exposes a query_spans tool supporting parameter filtering, child enumeration, pagination, sorting options (value, name, allocations, persistent-allocations), and recursive subtree searches.

export async function startTurboTraceServerCli(
  file: string,
  port: number | undefined,
  mcpPort: number | undefined
) {
  process.title = `next internal trace (v${process.env.__NEXT_VERSION})`
  const wsPort = port ?? DEFAULT_WS_PORT
  const httpPort = mcpPort ?? wsPort + 1
 
  let bindings
  try {
    bindings = await loadBindings()
  } catch {
    console.error(
      'Error: Could not load native bindings. The trace server requires native (non-WASM) bindings.'
    )
    process.exit(1)
  }
 
  let handle
  try {
    // Start the WebSocket trace server on a background thread (non-blocking).
    handle = bindings.turbo.startTurbopackTraceServerHandle(file, wsPort)
  } catch (err) {
    console.error(
      `Error: Could not start trace server for "${file}": ${err instanceof Error ? err.message : err}`
    )
    process.exit(1)
  }
 
  console.log(
    `Turbopack trace server started. View trace at https://trace.nextjs.org?port=${wsPort}`
  )
 
  // Create the MCP server.
  const mcpServer = new McpServer({
    name: 'Next.js Trace Server MCP',
    version: '0.1.0',
  })
 
  mcpServer.registerTool(
    'query_spans',
    {
      description: [
        'Query spans from a turbopack trace file: timing, CPU, attributes, allocation counters, and TurboMalloc live-memory samples.',
        '',
        'Navigation: pass a result `id` as `parent` to drill in. `search` is recursive over the whole subtree and returns full path IDs, so you can find a span without knowing where it lives. `depth` > 1 returns that many levels nested inline; `pageSize` (default 20, max 500) widens a page.',
        '',
        'Every response is paged: `totalCount` is how many matched in all, `totalPages` how many pages that is. Getting exactly `pageSize` results does not mean there are no more — check `totalCount`. Search cost scales with subtree size, so prefer scoping `parent` (or lowering `maxDepth`) over repeated root searches on a large trace.',
        '',
        'Allocations: `allocations` / `deallocations` / `allocationCount` and `persistentAllocations`, each with a `self*` counterpart excluding children. Comparing a total to its `self` shows whether a span allocates directly or only through descendants.',
        '',
        '`persistentAllocations` ranks allocators; it is NOT retained memory. It is allocated-minus-freed per TurboMalloc counters, which never see turbo-tasks cell or cache drops, so a total far above real peak RSS is expected rather than a leak. For absolute memory use `memorySummary` (count/start/end/min/peak/maxPressure, precomputed from `memorySamples`).',
        '',
        "But live heap is process-wide: a span's samples are just the global series sliced to its time range, so concurrent spans report identical memory however much each allocated. Rank concurrent work by the allocation fields, never by memory.",
        '',
        "Frees are charged to whichever span was on the stack at free time, not the one that allocated. A child with large `selfAllocations` under a parent with large `selfDeallocations` means the parent drops the child's arena — bounded, not leaking. Large `selfPersistentAllocations` with no such counterpart above it is the shape worth suspecting.",
        '',
        'For aggregated groups every allocation field is a group total, while `cpuDuration`, `correctedDuration` and `memorySamples` describe the example span only. `firstSpanId` is first in execution order; use `heaviestSpanId` to reach the member holding the most bytes.',
        '',
        'Set `outputType: "json"` for full precision and the raw `memorySamples` entries `[tsOffsetTicks, bytes, pressure, active_worker_threads]` (active_worker_threads excludes parked and blocking-pool threads); markdown is a human summary.',
      ].join('\n'),
      inputSchema: {
        parent: z
          .string()
          .optional()
          .describe(
            'Span ID to enumerate children of. Omit for root-level spans. Use the `id` field from a previous result.'
          ),
        aggregated: z
          .boolean()
          .optional()
          .describe(
            'When true (default), aggregate spans with the same name into a single entry. Set to false to see individual raw spans.'
          ),
        sort: z
          .enum(['value', 'name', 'allocations', 'persistent-allocations'])
          .optional()
          .describe(
            'Sort mode: "value" for corrected duration descending, "name" for alphabetical, "allocations" for total allocated bytes descending, "persistent-allocations" for `persistentAllocations` descending. Omit for execution order.'

Sources: packages/next/src/cli/internal/turbo-trace-server.ts:169-255

Build File Tracing and Memory Sampling

Output File Tracing and Dependency Collection

Next.js computes production file dependencies using Vercel’s @vercel/nft (nodeFileTrace) during the build pipeline. The trace coordination functions in collectBuildTraces process server entries, custom incremental cache handlers, and middleware boundaries, while filtering out non-essential compilation assets and standalone development server modules.

Artifact / Config KeyTarget Path / Processing RuleSources
next-server.js.nft.jsondistDir/next-server.js.nft.jsonpackages/next/src/build/collect-build-traces.ts
next-minimal-server.js.nft.jsondistDir/next-minimal-server.js.nft.jsonpackages/next/src/build/collect-build-traces.ts
outputFileTracingExcludesGlobs matching next-server added to additionalIgnorespackages/next/src/build/collect-build-traces.ts
Shared Ignore Patterns**/next/dist/compiled/next-server/**/*.dev.js, **/next/dist/compiled/jest-worker/**/*, **/next/dist/compiled/webpack/*, **/node_modules/webpack5/**/*packages/next/src/build/collect-build-traces.ts

Note

During standalone mode builds, extra IPC server files, startup wrappers, and requirement hooks are explicitly pushed into sharedEntriesSet to ensure complete runtime tracing outside the standard development environment.

Sources: packages/next/src/build/collect-build-traces.ts:94-232, packages/next/src/build/utils.ts:1600-1635

Periodic V8 Memory Heap Tracing and Snapshots

The memory tracing subsystem tracks process memory footprint across process lifetimes by scheduling recurring snapshots via startPeriodicMemoryUsageTracing and logging metrics (memory.rss, memory.heapUsed, memory.heapTotal, memory.heapMax) into active OpenTelemetry spans or standard trace outputs.

Metric KeyDescriptionSources
memory.rssResident Set Size obtained from process.memoryUsage()packages/next/src/lib/memory/trace.ts
memory.heapUsedUsed heap size obtained from v8.getHeapStatistics()packages/next/src/lib/memory/trace.ts
memory.heapTotalTotal heap size allocatedpackages/next/src/lib/memory/trace.ts
memory.heapMaxMaximum heap size limit (heap_size_limit)packages/next/src/lib/memory/trace.ts

Warning

When EXPERIMENTAL_DEBUG_MEMORY_USAGE is enabled and heap utilization exceeds HEAP_SNAPSHOT_THRESHOLD_PERCENT (70%), Next.js automatically writes a V8 heap snapshot to distDir on the first occurrence, guarded by the alreadyGeneratedHeapSnapshot flag to prevent duplicate disk writes.

Sources: packages/next/src/lib/memory/trace.ts:1-125

Was this page helpful?