Bun runtime
Run the standalone server under Bun — with per-file bytecode precompilation for measurably faster cold starts.
knext runs one runtime artifact — the standalone server.js that next build produces — and lets
you choose the process that executes it: Node (the default) or Bun. Bun is a first-class,
suite-tested runtime choice, and it unlocks a cold-start optimization Node doesn't have: per-file
bytecode precompilation at build time.
Enabling it
Set runtime: "bun" in your deploy config:
const config: KnativeNextConfig = {
name: 'acme',
registry: 'registry.example.com/acme',
runtime: 'bun', // 'node' (default) or 'bun'
// ...
};The NextApp resource carries the same field (spec.runtime: "bun"), and the operator runs the
container accordingly.
Verified against the official suite
The same official vercel/next.js deploy-mode e2e suite that validates the Node runtime
(778/778 nightly) also runs against Bun, on a weekly cadence:
- The most recent weekly run observed 775 of 778 tests passing under Bun — not yet a fully-green lane.
- The remaining failures are bugs we've isolated in the Bun runtime, with minimal
reproductions (the edge-sandbox outbound
fetch()hang is reduced to a completely Next-free repro; the other is an instrumentation-relatednot-foundinvariant). They persist in current Bun releases — Bun's canary line clears part of the set, but thefetch()hang persists even there — and they are re-tested as new Bun versions land.
knext does not blur the two claims: the Node runtime is the fully-green, nightly-verified credential; Bun is suite-tested weekly with the remaining gaps published rather than papered over.
Per-file bytecode precompilation
Scale-to-zero means cold starts, and the expensive part of a Next.js cold start is the JS engine
compiling the server bundle from source. When you build with runtime: 'bun', kn-next build
precompiles the standalone tree to JavaScriptCore bytecode ahead of time:
- Every server-side
.jsfile is transformed individually (the module graph is untouched — everyrequire()stays verbatim), emitting a companion.jscfile that Bun's runtime consumes onrequire(). - The
.jscis validated against its source: a stale, corrupt, or wrong-Bun-version companion silently falls back to executing the source. A version mismatch only forfeits the speed win. - The pass is fail-open: any file that can't be transformed is left byte-identical (with the
reason reported), and if the Bun binary can't do bytecode at all the whole pass is skipped.
Opt out entirely with
KNEXT_BUN_BYTECODE=0.
Measured on a real Next.js 16.2 standalone build (spawn → first dynamic-route response, N=12): 287 ms → 152 ms median (−47%). Bun's runtime transpiler cache composes on top (145 ms warm; see below).
The costs, stated plainly
- Build time: the pass spawns one
bun buildper file — roughly 12 s for a ~970-file standalone tree, paid on everyruntime: 'bun'build. - Image size: the
.jsccompanions roughly double-to-triple the standalone tree (37 MB → 95 MB on a minimal app). - The output is Bun-only — see the guard below.
A bytecode-built image only boots under Bun. A transformed file does not load under Node — and
it would fail silently there. knext makes this loud instead: after a successful bytecode pass,
the server entry gets a fail-fast guard, so node server.js on a bytecode-built image exits 1
with a FATAL message naming the fix (boot with Bun, or rebuild with runtime: 'node' /
KNEXT_BUN_BYTECODE=0) rather than crash-looping mutely in a pod. Switching a bytecode-built app
back to Node requires a rebuild. Images built for Node run under either runtime.
The runtime transpiler cache
Independently of the build-time bytecode pass, Bun caches its transpilation work at runtime. knext
points BUN_RUNTIME_TRANSPILER_CACHE_PATH at the same persistent volume that backs the deprecated
volume-backed bytecode caching (spec.cache.enableBytecodeCache — deprecated; the
compile cache is now baked into the image by default), so the cache survives pod
death and pays off across cold starts — worth roughly 20% alone, and composing with the bytecode
pass to 145 ms warm in the measurement above.
Because it rides on that same volume, it inherits the same constraint: the volume attaches to one
node at a time, so enabling it on an app that scales out across nodes leaves the extra pods
Pending. The build-time bytecode pass above has no such limit — it is baked into the image. See
bytecode caching.
The keep-alive guard (Bun ≤ 1.3)
Bun releases up to 1.3.x have a socket-reuse bug: a reused keep-alive connection can be reset
when the next request arrives immediately after the previous response — clients see sporadic
ECONNRESET / "socket hang up" on small, fast responses. knext ships a dependency-free preload
that mitigates it by advertising Connection: close on every response, so spec-honoring clients
(browsers, undici, the Knative activator) never reuse the socket.
The guard is loaded only on the Bun runtime, is a no-op under Node, and self-disables on Bun ≥ 1.4, where the underlying bug is fixed upstream.
Choosing a runtime
| Node (default) | Bun | |
|---|---|---|
| Official-suite status | 778/778, nightly | 775/778 most recent weekly (not yet green); the 3 remaining failures isolated to Bun runtime bugs |
| Cold-start compile cache | NODE_COMPILE_CACHE on a PVC | Build-time .jsc bytecode (−47% measured) + runtime transpiler cache on the PVC |
| Build cost | — | ~12 s extra per build; larger image |
| Image portability | Runs under Node and Bun | Bytecode-built images are Bun-only (loud guard) |
If you want the maximally-verified path, stay on Node. If cold-start latency is your priority and
you accept a Bun-only artifact, runtime: 'bun' is the fastest boot knext ships.