---
title: "How It Works"
description: "The architecture behind the binary — what the build adapter does at build time, what the runtime does at boot and per request."
---

> Documentation Index
> Fetch the complete documentation index at: https://ramonmalcolm10.github.io/next-bun-compile/llms.txt
> Use this file to discover all available pages before exploring further.

# How It Works

## Pipeline

1. **`next build`** runs your normal Next.js build. Because
   `adapterPath` points at next-bun-compile, Next invokes the adapter's
   `onBuildComplete` with its typed build outputs: every route's traced
   assets, prerender metadata (revalidate times, PPR state, headers),
   middleware matchers, and routing rules.
2. **The adapter assembles** a standalone-equivalent tree from those
   traced outputs — per-route assets, the server runtime's NFT trace,
   prerender seed files. `output: "standalone"` is never used.
3. **Code generation** scans the assembled tree, discovers
   externalized packages and turbopack-mangled aliases, computes which
   pages can be served frozen from memory, and generates
   `server-entry.js` + `assets.generated.js`.
4. **Bun compiles** the entry, the serve runtime, and every embedded
   asset file into one minified binary. Next.js itself is *not*
   bundled — it loads at runtime from the extracted tree, so nothing
   is carried twice.

## The three-tier runtime

A single `Bun.serve` owns the port. Requests route through the
fastest tier that can answer them correctly:

- **Tier 1 — static assets.** `/_next/static/*` and `public/*` served
  from memory with precompressed gzip, ETags, and immutable cache
  headers.
- **Tier 2 — frozen prerendered pages.** Pages with
  `revalidate: false`, no PPR postponed state, and no covering
  middleware or routing rule are served from memory with full RSC
  content negotiation. Non-GET methods (server actions!) always fall
  through to Next.
- **Tier 3 — everything else.** PPR resume, ISR, API routes, server
  actions, and dynamic rendering go to Next's own `router-server`
  request handler **in-process**, through a fetch→node bridge — the
  exact stack `next start` runs, minus its HTTP listener. ISR and
  cache-component responses additionally get an in-memory L1 cache.

**Revalidation stays Next's job.** The runtime patches Next's
incremental-cache wrapper — the layer that delegates to whichever
cache handler is configured — so every `revalidateTag`/`revalidatePath`
and background regeneration drops the affected page from the memory
tiers. Semantics are identical to `next start`, just faster between
changes. With a custom `cacheHandler` configured, the page tiers turn
themselves off entirely.

Next boots **lazily** on the first dynamic request; the static tiers
answer from the first millisecond the port is open.

## What gets embedded

- All static files in `.next/static/` (skipped if `assetPrefix` set)
- All public files
- Prerendered pages: HTML, RSC payloads, and their metadata
- The `.next/server/` tree (chunks, manifests, build ID)
- Every externalized server package — traced from your
  `node_modules/` into the binary as file assets
- The server orchestration graph (router-server chain), traced with
  the same pruning `output: "standalone"` applied

## Runtime: asset extraction

On first run, the binary writes its embedded assets to disk relative
to `process.execPath` — or to `NBC_RUNTIME_DIR` if set (point it at
tmpfs for RAM-backed runtime files and read-only root filesystems).
It then stamps `.next/.nbc-extracted` with a content hash of the
build plus the directory it extracted into. Later boots compare that
stamp: a match skips extraction entirely (one file read), while a
mismatch — new binary, moved directory, or a half-finished previous
extraction — re-extracts everything with overwrite so nothing stale
can shadow the embedded assets. Extraction is bounded to 64
concurrent writes so it can't exhaust file-descriptor limits.

## Resolver hook for externalized packages

Next.js with Turbopack externalizes some packages (`sharp`, `bcrypt`,
anything in `serverExternalPackages`, anything with native bindings) —
they're loaded at runtime instead of bundled. Two things make that
challenging in a compiled binary:

1. **Mangled names.** Turbopack rewrites `require("sharp")` to
   `require("sharp-457ea9eae1af1a9c")` in chunks, with a build-time
   symlink at `.next/node_modules/sharp-457... → sharp`. The
   compiled binary has no node_modules tree pre-baked.
2. **Bun's compiled-binary resolver.** Since bun 1.3.4 a compiled
   executable does not consult `node_modules` on disk unless built with
   `--compile-autoload-package-json`, which next-bun-compile passes. With
   it, an extracted package (e.g. `sharp/lib/sharp.js`) resolves its own
   dependencies from the extracted tree, ESM imports included. The CJS
   hook below still covers the cases bun's resolver rejects.

### Two mechanisms

#### 1. Build-time chunk rewrite

next-bun-compile scans server chunks for `"<name>-<16 hex>"` string
literals (the mangled-alias pattern) and replaces each with the
absolute file path of the canonical target:

```diff
- require("sharp-457ea9eae1af1a9c")
+ require("__NBC_BASE__/.next/node_modules/sharp/lib/index.js")
```

`__NBC_BASE__` is a placeholder substituted with the real `baseDir`
when `extractAssets` writes each chunk to disk. Resolution becomes a
direct file load — no walks, no exports-map ambiguity, no resolver
involvement. Works for both CJS `require()` and ESM `import()`.

For subpath imports (`import("prettier-.../plugins/html")`),
next-bun-compile reads each canonical's `package.json` + `exports` map
to pick the right `.mjs`/`.js`/`.cjs` variant, then rewrites to that
absolute path.

#### 2. Runtime `Module._resolveFilename` hook

Once execution is inside an extracted package (sharp's own
`require('detect-libc')`, dynamic interpolated requires like
`require('@img/sharp-PLATFORM/sharp.node')`), the chunk rewrite no
longer applies. A `Module._resolveFilename` hook catches CJS resolves
that bun's compiled-binary resolver fails on and reimplements
Node-compatible resolution from scratch:

- Walk node_modules from the calling file
- Read `package.json` + `main` for top-level
- Honor `exports` maps (`require`/`node`/`default` conditions)
- Handle directory subpaths with their own `package.json` +
  `main` (e.g. `next/dist/compiled/source-map`)

The hook also consults an alias map (`sharp-457... → sharp`) and
redirects mangled names that slip past the build-time rewrite.

## Module stubs (dev-only code paths)

Some modules can't be resolved at compile time but are never reached
in production — Next's dev server, the dev bundler, optional deps
loaded in try/catch. next-bun-compile creates no-op stubs for these
**only if** the real module isn't installed. If you do install
`@opentelemetry/api` or `critters`, the real package gets bundled
instead.

## Pre-flight validator (build time)

For every alias + subpath the chunks reference, next-bun-compile checks
whether resolution found a file. If anything didn't resolve, it
warns at build time:

```
next-bun-compile: ⚠ 1 of 4 turbopack alias reference(s) won't resolve at runtime:
  ✗ some-pkg-deadbeef/foo → some-pkg/foo
```

Set `NEXT_BUN_COMPILE_VERBOSE=1` during build to see every
resolution + the file it picked.

## Debug mode (runtime)

Set `NEXT_BUN_COMPILE_DEBUG=1` when running the binary to log every
resolver-hook decision: alias redirects, fallback walks, fallback
failures, with parent-file context. See
[Debug mode](/next-bun-compile/next-bun-compile/guides/debug-mode/).

Source: https://ramonmalcolm10.github.io/next-bun-compile/how-it-works/index.mdx
