---
title: "Hot Reloading"
description: "Hot Reloading powers the Next.js development experience by propagating code modifications, build status updates, and runtime errors instantly between the Development Server and the browser Client R..."
last_updated: "2026-10-12T06:38:56.853599+00:00"
canonical_url: "https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/development/hot-reloading"
---

<details>
<summary>Relevant source files</summary>

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

- [packages/next/src/server/dev/hot-reloader-turbopack.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-reloader-turbopack.ts)
- [packages/next/src/server/dev/hot-reloader-webpack.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-reloader-webpack.ts)
- [packages/next/src/client/dev/hot-reloader/app/web-socket.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/web-socket.ts)
- [packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx)
- [packages/next/src/client/dev/hot-reloader/pages/websocket.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/pages/websocket.ts)
- [packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts)
- [packages/next/src/server/dev/hot-middleware.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-middleware.ts)
- [packages/next/src/bundles/webpack/packages/JavascriptHotModuleReplacement.runtime.js](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/bundles/webpack/packages/JavascriptHotModuleReplacement.runtime.js)
- [packages/next/src/server/dev/on-demand-entry-handler.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/on-demand-entry-handler.ts)
- [packages/next/src/client/dev/hot-reloader/turbopack-hot-reloader-common.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/turbopack-hot-reloader-common.ts)
- [packages/next/src/bundles/webpack/packages/lazy-compilation-web.js](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/bundles/webpack/packages/lazy-compilation-web.js)
- [packages/next/src/client/page-bootstrap.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/page-bootstrap.ts)
- [packages/next/src/client/dev/hot-middleware-client.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-middleware-client.ts)
- [packages/next/src/bundles/webpack/packages/HotModuleReplacement.runtime.js](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/bundles/webpack/packages/HotModuleReplacement.runtime.js)
- [packages/next/src/client/dev/error-overlay/websocket.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/error-overlay/websocket.ts)
- [packages/next/src/client/dev/noop-turbopack-hmr.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/noop-turbopack-hmr.ts)
- [packages/next/src/server/dev/hot-reloader-shared-utils.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-reloader-shared-utils.ts)
- [packages/next/src/server/dev/messages.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/messages.ts)
- [packages/next/src/server/dev/parse-version-info.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/parse-version-info.ts)
- [packages/next/src/shared/lib/format-webpack-messages.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/format-webpack-messages.ts)
- [packages/next/src/shared/lib/invariant-error.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/invariant-error.ts)
</details>

## Overview

Hot Reloading powers the Next.js development experience by propagating code modifications, build status updates, and runtime errors instantly between the [Development Server](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/development/development-server) and the browser [Client Runtime](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/client-runtime/client-runtime). It coordinates both Webpack and Turbopack compiler architectures, managing WebSocket transports, binary message serialization, and client reconnection lifecycles to deliver updates across both [App Router](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/client-runtime/app-router) and Pages Router applications.

Sources: [packages/next/src/server/dev/hot-reloader-webpack.ts:454-490](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-reloader-webpack.ts#L454-L490), [packages/next/src/client/dev/hot-reloader/app/web-socket.ts:24-65](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/web-socket.ts#L24-L65), [packages/next/src/client/dev/hot-reloader/pages/websocket.ts:28-65](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/pages/websocket.ts#L28-L65)

## Server Webpack Hot Reloader Architecture

### Compiler Tracking and Middleware Initialization

`WebpackHotMiddleware` manages compilation states across the three Webpack compilers used in a Next.js development server: the client compiler, the server compiler, and the edge server compiler. During initialization, it attaches hooks to track compilation progress and errors for each compiler instance.

```typescript
compilers[0].hooks.invalid.tap('webpack-hot-middleware', this.onClientInvalid)
compilers[0].hooks.done.tap('webpack-hot-middleware', this.onClientDone)
compilers[1].hooks.invalid.tap('webpack-hot-middleware', this.onServerInvalid)
compilers[1].hooks.done.tap('webpack-hot-middleware', this.onServerDone)
compilers[2].hooks.done.tap('webpack-hot-middleware', this.onEdgeServerDone)
compilers[2].hooks.invalid.tap('webpack-hot-middleware', this.onEdgeServerInvalid)
```
Sources: [packages/next/src/server/dev/hot-middleware.ts:112-127](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-middleware.ts#L112-L127)

When the client compiler enters an invalid state, `onClientInvalid` broadcasts a building status message to connected browsers, provided the server compiler does not have active compilation errors. When the server or edge server compilers finish with errors, their respective stats are cached in `serverLatestStats` or `middlewareLatestStats`, ensuring build errors are preserved and delivered via the [developer overlay](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/development/developer-overlay).

Sources: [packages/next/src/server/dev/hot-middleware.ts:129-177](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-middleware.ts#L129-L177)

### Subscriber Management and WebSocket Upgrades

The hot reloader exposes an `onHMR` method that upgrades incoming HTTP connections to persistent WebSocket sessions via `wsServer.handleUpgrade`. Client connections are tracked either by an associated `htmlRequestId` for App Router requests or added to `clientsWithoutHtmlRequestId` for legacy or Pages Router clients.

| Collection / Map | Key Type | Value Type | Purpose |
| :--- | :--- | :--- | :--- |
| `clientsWithoutHtmlRequestId` | — | `ws` | Stores connected WebSocket clients that lack an explicit HTML request ID. |
| `clientsByHtmlRequestId` | `string` | `ws` | Maps specific App Router HTML request identifiers to their active socket clients. |
| `runtimeErrorStates` | `string` | `RuntimeErrorStateMessage` | Persists unhandled runtime error states per client when runtime error exposure is enabled. |

Sources: [packages/next/src/server/dev/hot-middleware.ts:81-87](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-middleware.ts#L81-L87), [packages/next/src/server/dev/hot-middleware.ts:189-196](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-middleware.ts#L189-L196)

When a client establishes connection, the middleware synchronizes the current build state by evaluating client and server statistics.

```typescript
function getStatsForSyncEvent(
  clientStats: { ts: number; stats: webpack.Stats } | null,
  serverStats: { ts: number; stats: webpack.Stats } | null
) {
  if (!clientStats) return serverStats?.stats
  if (!serverStats) return clientStats?.stats

  if (serverStats.stats.hasErrors()) {
    return serverStats.stats
  }

  return serverStats.ts > clientStats.ts ? serverStats.stats : clientStats.ts
}
```
Sources: [packages/next/src/server/dev/hot-middleware.ts:62-78](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-middleware.ts#L62-L78)

> [!WARNING]
> `getStatsForSyncEvent` explicitly prioritizes server compiler stats when they contain errors, preventing the client compilation stats from masking critical server-side build failures during initial synchronization.

Sources: [packages/next/src/server/dev/hot-middleware.ts:69-74](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-middleware.ts#L69-L74)

## Server Turbopack Hot Reloader Architecture

### WebSocket Upgrades and Client Registration

Turbopack hot reloading handles live development client connections by instantiating a dedicated WebSocket server (`wsServer`) via `ws.Server({ noServer: true })`. When an incoming request triggers `onHMR`, the reloader upgrades the connection using `wsServer.handleUpgrade`.

```typescript
onHMR(req, socket: Socket, head, onUpgrade) {
  wsServer.handleUpgrade(req, socket, head, (client) => {
    const clientIssues: EntryIssuesMap = new Map()
    const subscriptions: Map<string, AsyncIterator<any>> = new Map()

    const htmlRequestId = req.url
      ? new URL(req.url, 'http://n').searchParams.get('id')
      : null

    // Clients with a request ID are inferred App Router clients. If Cache
    // Components is not enabled, we consider those legacy clients. Pages
    // Router clients are also considered legacy clients. TODO: Maybe mark
    // clients as App Router / Pages Router clients explicitly, instead of
    // inferring it from the presence of a request ID.
    if (htmlRequestId) {
      clientsByHtmlRequestId.set(htmlRequestId, client)
      const enableCacheComponents = nextConfig.cacheComponents
      if (enableCacheComponents) {
        onUpgrade(client, { isLegacyClient: false })
        const cacheStatus = cacheStatusesByHtmlRequestId.get(htmlRequestId)
        if (cacheStatus !== undefined) {
          sendToClient(client, {
            type: HMR_MESSAGE_SENT_TO_BROWSER.CACHE_INDICATOR,
            state: cacheStatus,
          })
          cacheStatusesByHtmlRequestId.delete(htmlRequestId)
        }
      } else {
        onUpgrade(client, { isLegacyClient: true })
      }

      connectReactDebugChannelForHtmlRequest(
        htmlRequestId,
        sendToClient.bind(null, client)
      )

      sendSerializedErrorsToClientForHtmlRequest(
        htmlRequestId,
        sendToClient.bind(null, client)
      )
    } else {
      clientsWithoutHtmlRequestId.add(client)
      onUpgrade(client, { isLegacyClient: true })
    }
```
Sources: [packages/next/src/server/dev/hot-reloader-turbopack.ts:1539-1582](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-reloader-turbopack.ts#L1539-L1582)

Clients are categorized based on the presence of an HTML request identifier parsed from the query parameters (`id`). App Router connections carry an `htmlRequestId` and are registered in `clientsByHtmlRequestId`, whereas connections without an identifier fall back to `clientsWithoutHtmlRequestId` as legacy clients.

| Client Type / Map | Identification Method | Target Collection | Upgrade Options |
| :--- | :--- | :--- | :--- |
| **App Router Client** | Presence of URL search parameter `id` | `clientsByHtmlRequestId` | `{ isLegacyClient: false }` (if `cacheComponents` enabled) or `true` |
| **Legacy / Pages Client** | Absence of HTML request ID parameter | `clientsWithoutHtmlRequestId` | `{ isLegacyClient: true }` |

Sources: [packages/next/src/server/dev/hot-reloader-turbopack.ts:1544-1582](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-reloader-turbopack.ts#L1544-L1582)

> [!NOTE]
> When `cacheComponents` is active under the App Router, the server checks `cacheStatusesByHtmlRequestId` for any pre-existing cache status and immediately transmits a `CACHE_INDICATOR` message to the newly connected browser socket.

Sources: [packages/next/src/server/dev/hot-reloader-turbopack.ts:1555-1565](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-reloader-turbopack.ts#L1555-L1565)

## Binary Framing and Message Serialization

### Binary Message Packaging and Serialization

Next.js implements custom binary framing for specialized HMR messages sent from the server to the browser via WebSockets configured with `binaryType = 'arraybuffer'`. The server-side packaging function `createBinaryHmrMessageData` accepts a binary message payload and encodes it into a `Uint8Array`.

| Message Type Constant (`HMR_MESSAGE_SENT_TO_BROWSER`) | Value / Structure | Serialization Layout |
| :--- | :--- | :--- |
| `ERRORS_TO_SHOW_IN_BROWSER` | Binary error payload | `[0: uint8 type][1...end: serializedErrors byte array]` |
| `REACT_DEBUG_CHUNK` | Request ID and chunk payload | `[0: uint8 type][1: uint8 requestIdLength][2...2+len: encoded requestId][2+len...end: chunk bytes]` |

Sources: [packages/next/src/server/dev/messages.ts:12-58](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/messages.ts#L12-L58), [packages/next/src/client/dev/hot-reloader/app/web-socket.ts:64](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/web-socket.ts#L64)

> [!CAUTION]
> When serializing a `REACT_DEBUG_CHUNK` message, the `requestId` string length must not exceed 255 bytes; otherwise, an `InvariantError` is thrown because the single-byte length header cannot represent larger dimensions.

Sources: [packages/next/src/server/dev/messages.ts:32-36](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/messages.ts#L32-L36)

### Client Parsing and Invariant Verification

On the client browser socket (`packages/next/src/client/dev/hot-reloader/app/web-socket.ts`), incoming `MessageEvent` data instances of type `ArrayBuffer` are intercepted and passed to `parseBinaryMessage`. This function utilizes `DataView` and `TextDecoder` to unpack the binary frames back into typed HMR message objects.

```typescript
function parseBinaryMessage(data: ArrayBuffer): HmrMessageSentToBrowser {
  assertByteLength(data, 1)
  const view = new DataView(data)
  const messageType = view.getUint8(0)

  switch (messageType) {
    case HMR_MESSAGE_SENT_TO_BROWSER.ERRORS_TO_SHOW_IN_BROWSER: {
      const serializedErrors = new Uint8Array(data, 1)

      return {
        type: HMR_MESSAGE_SENT_TO_BROWSER.ERRORS_TO_SHOW_IN_BROWSER,
        serializedErrors,
      }
    }
    case HMR_MESSAGE_SENT_TO_BROWSER.REACT_DEBUG_CHUNK: {
      assertByteLength(data, 2)
      const requestIdLength = view.getUint8(1)
      assertByteLength(data, 2 + requestIdLength)

      const requestId = textDecoder.decode(
        new Uint8Array(data, 2, requestIdLength)
      )

      const chunk =
        data.byteLength > 2 + requestIdLength
          ? new Uint8Array(data, 2 + requestIdLength)
          : null

      return {
        type: HMR_MESSAGE_SENT_TO_BROWSER.REACT_DEBUG_CHUNK,
        requestId,
        chunk,
      }
    }
    default: {
      throw new InvariantError(
        `Invalid binary HMR message of type ${messageType}`
      )
    }
  }
}
```
Sources: [packages/next/src/client/dev/hot-reloader/app/web-socket.ts:255-295](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/web-socket.ts#L255-L295)

> [!WARNING]
> Every binary deserialization step invokes `assertByteLength` to verify that the incoming `ArrayBuffer` meets minimum length thresholds. If the buffer is truncated or smaller than expected, an `InvariantError` is thrown, indicating a malformed socket frame.

Sources: [packages/next/src/client/dev/hot-reloader/app/web-socket.ts:270-272](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/web-socket.ts#L270-L272), [packages/next/src/client/dev/hot-reloader/app/web-socket.ts:297-302](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/web-socket.ts#L297-L302)

## Client WebSocket Lifecycles and Reconnection

### Transport Connection Setup and Initialization

Next.js manages development HMR socket connections across App Router and Pages Router through dedicated modules located under `packages/next/src/client/dev/hot-reloader/`. For App Router environments, `createWebSocket` requires `self.__next_r` to be defined as a document request ID, throwing an `InvariantError` if it is absent. The connection URL is constructed via `getSocketUrl(assetPrefix)` combined with the HMR path endpoint and the request identifier.

```typescript
if (!self.__next_r) {
  throw new InvariantError(
    `Expected a request ID to be defined for the document via self.__next_r.`
  )
}
```
Sources: [packages/next/src/client/dev/hot-reloader/app/web-socket.ts:28-32](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/web-socket.ts#L28-L32)

> [!WARNING]
> Both App and Pages Router socket implementations enforce `WEB_SOCKET_MAX_RECONNECTIONS`. If continuous connection attempts exceed this threshold without a successful handshake, the client assumes the development server is no longer running and forces a full hard reload via `window.location.reload()`.

Sources: [packages/next/src/client/dev/hot-reloader/app/web-socket.ts:135-140](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/web-socket.ts#L135-L140), [packages/next/src/client/dev/hot-reloader/pages/websocket.ts:77-82](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/pages/websocket.ts#L77-L82)

### Heartbeats, Visibility, and Online Handlers

App Router sockets maintain a periodic ping mechanism through `useWebSocketPing`, which evaluates active layouts via `GlobalLayoutRouterContext`. When enabled outside of Turbopack, it dispatches JSON ping payloads every 2.5 seconds over the open socket.

```typescript
export function useWebSocketPing(webSocket: WebSocket | undefined) {
  const { tree } = useContext(GlobalLayoutRouterContext)

  useEffect(() => {
    if (!webSocket) {
      throw new InvariantError('Expected webSocket to be defined in dev mode.')
    }

    // Never send pings when using Turbopack as it's not used.
    // Pings were originally used to keep track of active routes in on-demand-entries with webpack.
    if (process.env.TURBOPACK) {
      return
    }

    // Taken from on-demand-entries-client.js
    const interval = setInterval(() => {
      if (webSocket.readyState === webSocket.OPEN) {
        webSocket.send(
          JSON.stringify({
            event: 'ping',
            tree,
            appDirRoute: true,
          })
        )
      }
    }, 2500)
    return () => clearInterval(interval)
  }, [tree, webSocket])
}
```
Sources: [packages/next/src/client/dev/hot-reloader/app/web-socket.ts:223-251](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/web-socket.ts#L223-L251)

> [!TIP]
> Both App and Pages Router socket clients listen for `visibilitychange` and `online` browser window events. When the document transitions to `visible` or network connectivity is restored while the WebSocket is not in an `OPEN` state, reconnection counters are reset and `init()` is immediately triggered to re-establish the transport channel.

Sources: [packages/next/src/client/dev/hot-reloader/app/web-socket.ts:156-176](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/web-socket.ts#L156-L176), [packages/next/src/client/dev/hot-reloader/pages/websocket.ts:98-118](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/pages/websocket.ts#L98-L118)

## App Router Update Dispatch and Diagnostics

### Message Dispatch and Webpack Update Application

Incoming HMR messages for the App Router are handled by `processMessage`, which routes messages based on their type. For hot updates, `handleHotUpdate` branches on whether Turbopack is active: under Turbopack, it queries `turbopackHmr.onBuilt()`, reports HMR latency, and updates the dispatcher state, whereas under Webpack, it initiates `tryApplyUpdatesWebpack`.

```tsx
export function processMessage(
  message: HmrMessageSentToBrowser,
  sendMessage: (message: string) => void,
  processTurbopackMessage: (msg: TurbopackMessageSentToBrowser) => void,
  staticIndicatorState: StaticIndicatorState
) {
  function handleErrors(errors: ReadonlyArray<unknown>) {
    // "Massage" webpack messages.
    const formatted = formatWebpackMessages({
      errors: errors,
      warnings: [],
    })

    // Only show the first error.
    dispatcher.onBuildError(formatted.errors[0])

    // Also log them to the console.
    for (let i = 0; i < formatted.errors.length; i++) {
      console.error(stripAnsi(formatted.errors[i]))
    }

    // Do not attempt to reload now.
    // We will reload on next success instead.
    if (process.env.__NEXT_TEST_MODE) {
      if (self.__NEXT_HMR_CB) {
        self.__NEXT_HMR_CB(formatted.errors[0])
        self.__NEXT_HMR_CB = null
      }
    }
  }

  function handleHotUpdate() {
    if (process.env.TURBOPACK) {
      const hmrUpdate = turbopackHmr!.onBuilt()
      if (hmrUpdate != null) {
        reportHmrLatency(
          sendMessage,
          [...hmrUpdate.updatedModules],
          hmrUpdate.startMsSinceEpoch,
          hmrUpdate.endMsSinceEpoch,
          // suppress the `client-hmr-latency` event if the update was a no-op:
          hmrUpdate.hasUpdates
        )
      }
      dispatcher.onBuildOk()

      // A successful build can be a no-op after recovering from an error, so
      // there may be no server component change to resolve the test callback.
      if (process.env.__NEXT_TEST_MODE && self.__NEXT_HMR_CB) {
        self.__NEXT_HMR_CB()
        self.__NEXT_HMR_CB = null
      }
    } else {
      tryApplyUpdatesWebpack(sendMessage)
    }
  }
```
Sources: [packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx:223-278](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx#L223-L278)

> [!NOTE]
> `tryApplyUpdatesWebpack` checks `isUpdateAvailable()` and `canApplyUpdates()` before calling `module.hot.check(false)` followed by `module.hot.apply()`. If a new update arrives while an update is already in progress, `tryApplyUpdatesWebpack` recursively triggers itself to apply the latest compilation state.

Sources: [packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx:148-220](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx#L148-L220)

### Error Formatting and Walkthrough

When build errors occur, `processMessage` delegates to `handleErrors`, which massages Webpack error payloads and logs them via `stripAnsi` and `console.error`. The execution path follows a explicit call chain from message handling through diagnostic formatting.

```mermaid
sequenceDiagram
    participant web-socket.ts
    participant hot-reloader-app.tsx
    participant format-webpack-messages.ts
    web-socket.ts->>hot-reloader-app.tsx: processMessage(message, ...)
    hot-reloader-app.tsx->>format-webpack-messages.ts: formatWebpackMessages({ errors, warnings: [] })
    format-webpack-messages.ts->>format-webpack-messages.ts: formatMessage(message, verbose, importTraceNote)
```

1. `processMessage` receives the HMR browser message and invokes `handleErrors` when error arrays are present.
2. `handleErrors` passes raw error collections into `formatWebpackMessages`.
3. `formatWebpackMessages` maps each error through `formatMessage` to clean up Webpack-specific headers, loader paths, module traces, and syntax error labels.
4. `formatMessage` returns the normalized string which `dispatcher.onBuildError` and the console logger display.

```tsx
function handleErrors(errors: ReadonlyArray<unknown>) {
  // "Massage" webpack messages.
  const formatted = formatWebpackMessages({
    errors: errors,
    warnings: [],
  })

  // Only show the first error.
  dispatcher.onBuildError(formatted.errors[0])

  // Also log them to the console.
  for (let i = 0; i < formatted.errors.length; i++) {
    console.error(stripAnsi(formatted.errors[i]))
  }

  // Do not attempt to reload now.
  // We will reload on next success instead.
  if (process.env.__NEXT_TEST_MODE) {
    if (self.__NEXT_HMR_CB) {
      self.__NEXT_HMR_CB(formatted.errors[0])
      self.__NEXT_HMR_CB = null
    }
  }
}
```
Sources: [packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx:229-252](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx#L229-L252), [packages/next/src/shared/lib/format-webpack-messages.ts:217-256](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/format-webpack-messages.ts#L217-L256)

> [!CAUTION]
> `formatWebpackMessages` searches the formatted error array for `ReactServerComponentsError`. If found, it splices that specific error out and moves it to the front of the errors array (`formattedErrors.unshift`) so server component build failures take precedence in the overlay.

Sources: [packages/next/src/shared/lib/format-webpack-messages.ts:228-244](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/format-webpack-messages.ts#L228-L244)

## Pages Router Update and Module Replacement

### Hot Middleware Client Events and Page Lifecycle Routing

The Pages Router update client is initialized via `hot-middleware-client.ts`, which registers a custom HMR event subscriber to process server-driven page changes and reload triggers. When `RELOAD_PAGE` is received, the client sends a `client-reload-page` telemetry message over the websocket, sets its internal reloading flag, and invokes `window.location.reload()`. For `REMOVED_PAGE` and `ADDED_PAGE` messages, the client extracts the target page string and compares it against `router.pathname` and error page indicators (`/404` or `/_error`). 

```typescript
export default () => {
  const devClient = connect()

  devClient.subscribeToHmrEvent((message) => {
    if (reloading) return

    // Retrieve the router if it's available
    const router = window.next?.router

    // Determine if we're on an error page or the router is not initialized
    const isOnErrorPage =
      !router || router.pathname === '/404' || router.pathname === '/_error'

    switch (message.type) {
      case HMR_MESSAGE_SENT_TO_BROWSER.RELOAD_PAGE: {
        sendMessage(
          JSON.stringify({
            event: 'client-reload-page',
            clientId: window.__nextDevClientId,
          })
        )
        reloading = true
        return window.location.reload()
      }
      case HMR_MESSAGE_SENT_TO_BROWSER.REMOVED_PAGE: {
        const [page] = message.data

        // Check if the removed page is the current page
        const isCurrentPage = page === router?.pathname

        // We enter here if the removed page is currently being viewed
        // or if we happen to be on an error page.
        if (isCurrentPage || isOnErrorPage) {
          sendMessage(
            JSON.stringify({
              event: 'client-removed-page',
              clientId: window.__nextDevClientId,
              page,
            })
          )
          return window.location.reload()
        }
        return
      }
      case HMR_MESSAGE_SENT_TO_BROWSER.ADDED_PAGE: {
        const [page] = message.data

        // Check if the added page is the current page
        const isCurrentPage = page === router?.pathname

        // Check if the page component is not yet loaded
        const isPageNotLoaded =
          page !== null && typeof router?.components?.[page] === 'undefined'

        // We enter this block if the newly added page is the one currently being viewed
        // but hasn't been loaded yet, or if we're on an error page.
        if ((isCurrentPage && isPageNotLoaded) || isOnErrorPage) {
          sendMessage(
            JSON.stringify({
              event: 'client-added-page',
              clientId: window.__nextDevClientId,
              page,
            })
          )
          return window.location.reload()
        }
        return
      }
      case HMR_MESSAGE_SENT_TO_BROWSER.DEV_PAGES_MANIFEST_UPDATE: {
        return
      }
      default: {
        message satisfies never
      }
    }
  })
```
Sources: [packages/next/src/client/dev/hot-middleware-client.ts:24-99](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-middleware-client.ts#L24-L99)

> [!WARNING]
> A page addition triggers a hard browser reload only if the added page matches the current route pathname and the corresponding component cache entry in `router.components[page]` is undefined, or if the view is currently on an error page.

Sources: [packages/next/src/client/dev/hot-middleware-client.ts:68-91](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-middleware-client.ts#L68-L91)

### Compilation State Handlers and Turbopack Integration

In `hot-reloader-pages.ts`, incoming messages are processed through `handleSuccess`, `handleWarnings`, and `handleErrors`. Successful compilations clear outdated console errors and check whether `process.env.TURBOPACK` is enabled; if so, it invokes `turbopackHmr.onBuilt()`, records HMR latency, and dispatches `dispatcher.onBuildOk()`. Under Webpack, it determines if an update is a hot update based on compilation history and router state before calling `tryApplyUpdatesWebpack()`.

```typescript
function handleSuccess() {
  clearOutdatedErrors()
  hasCompileErrors = false

  if (process.env.TURBOPACK) {
    const hmrUpdate = turbopackHmr!.onBuilt()
    if (hmrUpdate != null) {
      reportHmrLatency(
        sendMessage,
        [...hmrUpdate.updatedModules],
        hmrUpdate.startMsSinceEpoch,
        hmrUpdate.endMsSinceEpoch,
        hmrUpdate.hasUpdates
      )
    }
    dispatcher.onBuildOk()
  } else {
    const isHotUpdate =
      !isFirstCompilation ||
      (window.__NEXT_DATA__.page !== '/_error' && isUpdateAvailable())

    // Attempt to apply hot updates or reload.
    if (isHotUpdate) {
      tryApplyUpdatesWebpack()
    }
  }

  isFirstCompilation = false
}
```
Sources: [packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts:142-170](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts#L142-L170), [packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts:274-307](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts#L274-L307)

Warnings invoke `formatWebpackMessages` with zero errors, limit console output to the first five warnings, strip ANSI escape codes, and subsequently attempt to apply updates if it is a hot update compilation. Errors suppress reloading entirely, formatting error arrays and forwarding the primary build error to `dispatcher.onBuildError()` and `console.error`.

```typescript
function handleWarnings(warnings: any) {
  clearOutdatedErrors()

  const isHotUpdate = !isFirstCompilation
  isFirstCompilation = false
  hasCompileErrors = false

  function printWarnings() {
    // Print warnings to the console.
    const formatted = formatWebpackMessages({
      warnings: warnings,
      errors: [],
    })

    if (typeof console !== 'undefined' && typeof console.warn === 'function') {
      for (let i = 0; i < formatted.warnings?.length; i++) {
        if (i === 5) {
          console.warn(
            'There were more warnings in other files.\n' +
              'You can find a complete log in the terminal.'
          )
          break
        }
        console.warn(stripAnsi(formatted.warnings[i]))
      }
    }
  }

  printWarnings()

  // Attempt to apply hot updates or reload.
  if (isHotUpdate) {
    tryApplyUpdatesWebpack()
  }
}

// Compilation with errors (e.g. syntax error or missing modules).
function handleErrors(errors: any) {
  clearOutdatedErrors()

  isFirstCompilation = false
  hasCompileErrors = true

  // "Massage" webpack messages.
  var formatted = formatWebpackMessages({
    errors: errors,
    warnings: [],
  })

  // Only show the first error.

  dispatcher.onBuildError(formatted.errors[0])

  // Also log them to the console.
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    for (var i = 0; i < formatted.errors.length; i++) {
      console.error(stripAnsi(formatted.errors[i]))
    }
  }

  // Do not attempt to reload now.
  // We will reload on next success instead.
  if (process.env.__NEXT_TEST_MODE) {
    if (self.__NEXT_HMR_CB) {
      self.__NEXT_HMR_CB(formatted.errors[0])
      self.__NEXT_HMR_CB = null
    }
  }
}
```
Sources: [packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts:173-241](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts#L173-L241)

### Module Invalidation and Replacement Runtime

Webpack HMR module replacement runtime manages state transitions across five discrete status values within the module system: `idle`, `check`, `prepare`, `dispose`, and `apply`. Modules support self-acceptance, dependency acceptance callbacks, error handlers, and explicit invalidation workflows.

| Status | Trigger / Condition | Action / Next State |
| :--- | :--- | :--- |
| `idle` | Default state or post-apply cleanup | Allows `check()` execution; triggers manifest downloads |
| `check` | `hotCheck()` invocation | Downloads update manifest; transitions to `prepare` or returns `idle` |
| `prepare` | Manifest downloaded with updates | Tracks blocking promises; transitions to `dispose` via `internalApply()` |
| `dispose` | Execution of module dispose handlers | Disables active modules, clears cache entries, strips parent/child links |
| `apply` | Insertion of module factories | Executes new runtime modules, call accept handlers, loads self-accepted modules |

```javascript
// status
var registeredStatusHandlers = [];
var currentStatus = "idle";

// while downloading
var blockingPromises = 0;
var blockingPromisesWaiting = [];

// The update info
var currentUpdateApplyHandlers;
var queuedInvalidatedModules;

$hmrModuleData$ = currentModuleData;

$interceptModuleExecution$.push(function (options) {
	var module = options.module;
	var require = createRequire(options.require, options.id);
	module.hot = createModuleHotObject(options.id, module);
	module.parents = currentParents;
	module.children = [];
	currentParents = [];
	options.require = require;
});

$hmrDownloadUpdateHandlers$ = {};
$hmrInvalidateModuleHandlers$ = {};

function createRequire(require, moduleId) {
	var me = installedModules[moduleId];
	if (!me) return require;
	var fn = function (request) {
		if (me.hot.active) {
			if (installedModules[request]) {
				var parents = installedModules[request].parents;
				if (parents.indexOf(moduleId) === -1) {
					parents.push(moduleId);
				}
			} else {
				currentParents = [moduleId];
				currentChildModule = request;
			}
			if (me.children.indexOf(request) === -1) {
				me.children.push(request);
			}
		} else {
			console.warn(
				"[HMR] unexpected require(" +
					request +
					") from disposed module " +
					moduleId
			);
			currentParents = [];
		}
		return require(request);
	};
	var createPropertyDescriptor = function (name) {
		return {
			configurable: true,
			enumerable: true,
			get: function () {
				return require[name];
			},
			set: function (value) {
				require[name] = value;
			}
		};
	};
	for (var name in require) {
		if (Object.prototype.hasOwnProperty.call(require, name) && name !== "e") {
			Object.defineProperty(fn, name, createPropertyDescriptor(name));
		}
	}
	fn.e = function (chunkId, fetchPriority) {
		return trackBlockingPromise(require.e(chunkId, fetchPriority));
	};
	return fn;
}

function createModuleHotObject(moduleId, me) {
	var _main = currentChildModule !== moduleId;
```
Sources: [packages/next/src/bundles/webpack/packages/HotModuleReplacement.runtime.js:27-188](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/bundles/webpack/packages/HotModuleReplacement.runtime.js#L27-L188), [packages/next/src/bundles/webpack/packages/HotModuleReplacement.runtime.js:259-390](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/bundles/webpack/packages/HotModuleReplacement.runtime.js#L259-L390)

> [!NOTE]
> When a module calls `invalidate()`, `_selfInvalidated` is set to `true`. If called during `prepare`, `check`, `dispose`, or `apply` states, the module ID is pushed to `queuedInvalidatedModules` for execution during the subsequent apply pass.

Sources: [packages/next/src/bundles/webpack/packages/HotModuleReplacement.runtime.js:155-188](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/bundles/webpack/packages/HotModuleReplacement.runtime.js#L155-L188)

## Related

- [Development Server](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/development/development-server)
- [Developer Overlay](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/development/developer-overlay)
- [Turbopack Engine](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/turbopack-engine)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/llms.txt) for all pages in this wiki.
