knext

Bun & the compiled executable

knext's default build compiles your Next.js server into a bytecode executable with the Bun runtime baked in. The selectable vinext build compiles a single self-contained executable.

knext compiles your server ahead of time. Two builds do it, and this page covers both:

  • The default (build: 'turbopack', runtime: 'bun') compiles Next's standalone server.js into a Bun bytecode executable. The executable runs inside the standalone folder and loads each route's chunks from .next/server beside it. See The standalone target on Bun is compiled too.
  • The vinext build (build: 'vinext') compiles your whole app into one executable, with the Bun runtime baked in. The numbers below were measured on this build.

Precompiling to bytecode means the server boots in tens of milliseconds instead of parsing your source at startup — the boot phase where a Next.js app otherwise spends real CPU.

Which runtime should I use?

Bun is the default for new apps, and the recommended choice for most of them. Each reason below carries its own condition — none of them is "Bun is faster":

  • The ISR/data cache's Redis client works out of the box. The Bun runtime's standalone build and the compiled executable both load the Redis client @getknext/core ships with; on the Node runtime's standalone build, that same client can silently fail to load, and the cache falls back to a per-pod in-memory cache instead of the shared Redis one. See the known issue on the ISR & the Redis cache page for the exact affected combination, the symptoms, and the workaround — it's fixed in a later patch release.
  • The compiled single-executable build is Bun-only. The vinext build target — your whole app compiled into one binary with the Bun runtime and its bytecode baked in — has no Node equivalent; see the vinext build above.
  • Cold start is cluster-dependent, and the gap is small or absent, not a Bun win. On Oracle Kubernetes Engine (2 shared-core 4-vCPU nodes, images pre-pulled, a default-scaffold app with no database/cache/storage dependency, n=10 per combination), the four runtime x builder combinations cold-started within about 0.3 s of each other at the median (roughly 2.1-2.4 s), and none of the pairwise differences was distinguishable from measurement noise. On that same cluster, warm requests were slightly faster on Node (~24 ms) than on Bun (~31 ms) — a real but small (~7 ms) difference, negligible next to a cold start. On Google Kubernetes Engine, moving the same pod from a shared-core 4-vCPU machine type to a dedicated-core one of the next generation shrank the runtime gap from roughly 0.7 s (Bun ahead, ten wakes per runtime) to roughly 0.1 s (not distinguishable from noise at that sample size) — see Runtime choice for the full comparison and its conditions. Measure your own app and cluster rather than picking a runtime for cold start alone.

Choose the Node runtime instead if: your app or a dependency relies on a Node-only API or a native module Bun doesn't support, your team standardizes its tooling on Node, or you need Node-specific build or debugging tooling. Node is fully supported — the official Next.js adapter runs on it the same way it runs on Bun — and it is covered by the same compatibility credential (still in progress) as Bun: two of the four runtime x builder combinations in that credential (Turbopack and Webpack) are Node's.

The numbers

All figures are the same app and the same route, responses verified byte-identical, ten samples each with a fresh local process and port — i.e. this measures the server's own boot, not an end-to-end deployment cold start. They were measured on the vinext compiled executable; the default standalone executable was not part of this benchmark, so do not read these as its numbers:

TargetProcess boot (median)Requests/sec
Node standalone884 ms630
Bun 1.4 standalone703 ms714
vinext compiled executable61 ms1103

The vinext compiled executable boots 14× faster and serves 75% more requests per second than the Node standalone. It wins both axes at once because the whole bundle is compiled together: minification and bundling remove the per-module boundary cost that made earlier file-by-file bytecode approaches trade throughput for cold start.

These are process-boot numbers from a fresh local process. On Knative scale-from-zero the end-to-end cold start a user pays is dominated by the platform — activation, scheduling, and container start — which on a real cluster is measured comparable across targets, so the boot win is one component of that budget rather than the whole of it. The compiled executable's boot advantage pays off most where that platform overhead is small or pre-warmed. See Scale-to-zero & cold starts for the full budget.

Nothing to configure

The compiled Bun executable is the default. A minimal config builds and deploys it:

knext.config.ts
const config: KnativeNextConfig = {
  name: 'acme',
  registry: 'registry.example.com/acme',
  // ...
};

Your app needs output: 'standalone' in next.config.ts and a build script that runs next build. knext create sets both up. knext build compiles the server, and knext deploy builds it into the image. The NextApp resource records the artifact (spec.build: "turbopack" and spec.runtime: "bun"), and the operator starts the container.

To compile the vinext build instead, set build: 'vinext'; knext build then produces one binary (plus the native image-optimization module that ships beside it), and the executable is the server.

Building requires Bun 1.4 or newer on your PATH. The knext CLI itself runs under plain Node — only the compile step shells out to bun. If Bun is missing, the build stops and tells you how to install it; if Bun is present but bun --version fails for another reason, the build reports that underlying error instead of claiming Bun is not installed. Older versions are refused rather than silently producing a binary that boots roughly twice as slowly — upgrade with bun upgrade.

Warm-on-boot (vinext build)

This section applies to the vinext build, whose scaffolded server entry does the warming. On the default standalone build, KNEXT_WARM_PATH only chooses the paths warmed while the image bakes its compile cache; nothing fires at pod boot.

When a scaled-to-zero vinext app wakes, the very first request pays the full cost of a cold process — JIT warm-up and the first render. To keep a real visitor from paying it, the runtime fires a synthetic request at your own app the moment it boots, overlapped with becoming ready, so the hot path is already warm when the first user arrives. This is on by default; you don't need to configure anything.

Two environment variables tune it:

  • KNEXT_WARM_PATH — a comma-separated list of paths to warm, in order (default /api/health). The first fires immediately; the rest follow one at a time, so a contended cold CPU isn't fighting several warm requests at once. Point it at the routes whose first render is expensive:

    knext.config.ts
    const config: KnativeNextConfig = {
      name: 'acme',
      env: {
        KNEXT_WARM_PATH: '/,/api/health',
      },
    };
  • KNEXT_EAGER_WARM=0 — turns warm-on-boot off entirely.

Only warm paths that render the same for every anonymous visitor. The warm request is synthetic — it carries no cookies and no authentication — and its response is cached. Warming a per-user or authenticated page would prime the cache with the logged-out render and serve it to the first real user. / is a good choice only when your home page is identical for everyone; otherwise leave the default /api/health.

Set these in your knext.config.ts env: block (they become spec.env on the NextApp), the same way as any other runtime environment variable.

The vinext build: your app must be an ES module

The vinext build requires your app to be an ES module — your package.json must set:

package.json
{
  "type": "module"
}

knext create sets this for you, so a freshly scaffolded app already satisfies it and you never have to think about it. The requirement matters only when you point the vinext build at an existing Next.js app that was written as CommonJS: add "type": "module" to its package.json before you build. Without it, the build fails while wiring up React Server Components — the server/client module graph cannot be resolved — rather than producing a broken binary.

If your project still contains CommonJS files that rely on require() or a .js extension meaning CommonJS, rename them to .cjs (or convert them to import/export) so they keep working under "type": "module".

Image optimization is included

The compiled binary cannot load native modules from inside itself, so knext ships the image codec (sharp) beside the executable and loads it by path. /_next/image serves optimized AVIF/WebP exactly as it does on a folder deployment — verified in CI against the production image. See Image optimization.

What the vinext executable is, plainly

  • One file. The server, every route it reaches, and the Bun runtime, compiled together (~70 MB). Static assets and the native image codec ship alongside it in the image.
  • Bytecode ahead of time. The engine never parses your server source at boot — that work happened at build time, which is where most of the 14× comes from.
  • Linux x64. The build cross-compiles for the deployment platform regardless of the machine you build on.
  • Runtime is baked in. On this build the runtime config field does not select anything for the compiled executable; the Bun runtime ships inside the binary. (runtime: 'node' selects a separate uncompiled Node server for vinext — see vinext on Node.)

Verification status, stated honestly

The default cell — Turbopack build on the compiled Bun executable — passed the official suite in dispatched runs (792 passed, 0 failed at Next.js v16.2.12, and 1101 of 1101 at v16.3.8), with every deploy confirmed to boot the bytecode executable. It is not yet credentialed: see the compatibility credential. The vinext build is a different artifact. It is measured, not credentialed: its latest run passed 1016 of 1099 files (Next.js v16.3.6). Of the rest, 41 are disclosed known-gap files (vinext does not implement the Next.js 16.3 client-router architecture), 6 are individually quarantined and 36 fail. On the official suite minus those 41 disclosed known-gap files, 1016 of 1058 pass; vinext remains Beta. Read the per-feature status on the compatibility matrix rather than assuming one number carries over.

The standalone target on Bun is compiled too

The selectable standalone targets (build: 'turbopack' or build: 'webpack' — identical artifacts, different next build bundler; see the build pipeline) run next build's own server. With runtime: 'bun', knext build compiles that server into a Bun single executable with bytecode as well, and the image runs the executable instead of bun server.js:

  • What is inside. Next's server entry and everything it loads up front — the request pipeline, the router, the render entry points — bundled, minified and precompiled to bytecode.
  • What stays beside it. The standalone folder itself. Next loads each route's compiled chunk from .next/server by path when the route is first requested, so the executable sits inside that folder in the image and serves the tree next to it.
  • One copy of every shared module. Any module that both the executable and the files beside it load — Next's internal singletons, React, the Pages Router runtime — is left on disk and loaded from there by both, so each exists exactly once in the process. That includes the modules your custom cacheHandler (and any cacheHandlers entry) imports, so a handler may use Next internals safely. Write those imports as plain string literals (require('next/dist/…')): a module path computed at runtime cannot be seen at build time.
  • The cache handler must ship with the app. Configure cacheHandler as a file inside your project so next build copies it into the standalone folder. If the configured handler is not there, knext build fails and names the file, because it cannot check what that handler imports.
  • Bytecode is checked, not assumed. After compiling, knext build inspects the executable and fails the build if the server was not compiled to bytecode. A binary without it would boot and serve — just slower on every cold start — so it is refused rather than shipped.
  • Build requirements. Bun 1.4 or newer on the machine running knext build, and output: 'standalone' in next.config.ts. A build with no standalone server fails instead of producing an image without the executable.

With runtime: 'node' nothing is compiled: Node's bytecode caching for this target is the V8 compile cache baked into the image (see bytecode caching).

The retired per-file bytecode pass

Earlier knext versions offered an optional per-file bytecode pass for the standalone folder under runtime: 'bun'. That pass is retired: it improved boot (554 ms vs 703 ms) but reduced throughput (537 req/s against Node's 630), because converting each module individually taxes every module boundary on every request. Whole-bundle compilation — of the vinext build, and of the standalone server on Bun as described above — replaced it.

Known limitation: turbopackModuleFragments

Next's experimental turbopackModuleFragments flag is not supported on the Bun runtime with a Turbopack build. With it on, Turbopack emits chunks that assign to a const inside code that never runs. Node accepts that; Bun's parser rejects it when the chunk loads, so the app fails at runtime.

knext build, knext deploy and knext preview therefore all refuse the combination (Bun, Turbopack build, flag on) before any build work, and knext doctor reports it. To proceed, either turn experimental.turbopackModuleFragments off in your next.config, set runtime: 'node', or use a webpack build, which does not emit those chunks. The restriction lifts once a Bun release fixes the parser.

Two limits of the check. It reads the flag literally from next.config, so a flag computed at runtime is not detected. And knext deploy --skip-build ships an already-built artifact without re-checking it.

On this page