Bytecode caching
Skipping JS recompilation on cold pods — a compile cache baked into your image on Node, per-file .jsc precompilation on Bun.
Scale-to-zero means cold starts. The expensive part of a Next.js cold start is the JS engine compiling JavaScript from source. knext skips that work on both runtimes by doing the compilation at build time and shipping the result inside your application image: on Node as a V8 compile cache, and on Bun as per-file bytecode (see Bun runtime). This page covers the Node mechanism.
There is nothing to configure and nothing to enable. The cache is built when your image is built, so it is present from the very first pod, on any cluster.
How it works (Node)
During the image build, knext boots your server once and lets V8 serialize its compiled bytecode to a cache directory inside the image. At runtime the server reads that cache instead of parsing and compiling your application's JavaScript again.
Because the cache lives in the image layer:
- It works on a stock Kubernetes or Knative install. No storage class, no volume, no cluster storage configuration of any kind.
- It is there on the first cold pod, not merely on later ones. A pod that wakes from zero reads a cache that was already populated before the image shipped.
- It never goes stale. The cache is versioned with the image it lives in, so a new build always carries a cache that matches the code it was built from.
- It does not constrain how wide you scale. Every pod carries its own copy, so there is no shared resource to contend over and no limit on how many nodes your app spreads across.
The trade-off is image size: the cache adds a layer of serialized bytecode to every image.
The build fails if the cache comes out empty. A compile cache that is silently never populated looks exactly like one that works — same configuration, same startup path, no error — while delivering nothing. The build checks that the cache was actually written rather than trusting that it was.
This is independent of your data cache. Bytecode caching governs how fast the server boots;
the cache provider (redis, and friends) governs where ISR and data are cached. The two are
unrelated and neither one turns the other on.
What to expect
Skipping compilation removes real work from every cold start, and the effect is measurable on the server-boot portion of a cold start. How much of your end-to-end cold start that represents depends heavily on your cluster: on a small cluster where pod scheduling and image pull dominate, compilation is a smaller slice of the total than on a fast, warm-image cluster.
Treat it as a real but environment-dependent improvement, not a guaranteed number. Measure your own cold start if it matters to your service-level objectives.
If the cache directory is unavailable
Bytecode caching is an optimisation, never a dependency: if the cache directory cannot be used, your app still boots and serves normally — it simply pays full compile cost on each cold start.
That covers every way the directory can go wrong: it was never created, it points at a volume that did not mount, it is read-only or otherwise unwritable, it is a file rather than a directory, or it holds a half-written entry left behind by a pod that was killed mid-write. None of these fail a boot, and none of them need a fallback setting from you.
The trade-off is that a broken cache directory is otherwise silent — the app looks healthy and is just slower forever. So knext logs a single warning shortly after startup when it detects that the cache directory it was given was refused:
NODE_COMPILE_CACHE=/mnt/cache/bytecode was refused by the runtime (unwritable, unmounted, or not a
directory); the server is serving normally but WITHOUT bytecode reuse, so every cold start pays full
compile cost.If you see it, check the path you pointed the cache at — most often a volume that is mounted read-only or is missing entirely.
The warning is deliberately narrow, so these cases stay quiet:
- You did not override the cache location. The default lives inside the image, where it is always readable.
- You turned bytecode caching off with Node's
NODE_DISABLE_COMPILE_CACHE. That is a deliberate choice, not a broken volume, so it is reported as disabled rather than warned about — and note that Node treats the variable's presence as "off", includingNODE_DISABLE_COMPILE_CACHE=0. - You run the Bun runtime. Bun does not implement this cache at all (it has its own mechanism, below), so there is nothing to report either way.
Node vs Bun
Both runtimes get a compile cache, built at image build time, via different mechanisms:
| Runtime | Bytecode mechanism |
|---|---|
| Node (default) | V8 compile cache, populated during the image build and shipped in the image. Read by every pod from its first boot. |
Bun (runtime: 'bun') | Per-file bytecode precompilation: kn-next build emits a companion .jsc for every server-side file (measured −47% startup, 287 ms → 152 ms median), plus Bun's runtime transpiler cache. See Bun runtime. |
A Bun bytecode-built image is Bun-only — booting it with Node exits 1 with a FATAL message
naming the fix rather than crash-looping silently. Details and costs are on the
Bun runtime page.
If you enabled the volume-backed cache
Deprecated. The opt-in PVC-backed bytecode cache (bytecodeCache.enabled /
spec.cache.enableBytecodeCache) is deprecated and will be removed in a future release. The compile
cache is now baked into the image by default, so you no longer need to enable this — remove the
bytecodeCache block from your config. If you keep the volume-backed cache, knext will warn you.
Earlier versions of knext kept the Node compile cache on a persistent volume instead, so that it survived pod death. That approach is superseded and you should not enable it on a new deployment:
- Most Kubernetes and Knative installs reject the volume outright unless a cluster administrator has turned on optional persistent-volume support, so the app simply fails to start.
- Where it does work, the volume attaches to one node at a time, so a pod scheduled onto a second node sits Pending instead of serving traffic — capping how wide the app can scale.
The build-time cache has neither limitation and needs no configuration, so if your config still
carries a bytecodeCache block you can remove it, or turn it off explicitly:
export default {
cache: { provider: 'redis', url: process.env.REDIS_URL },
bytecodeCache: { enabled: false },
}Turning it off does not disable bytecode caching — the cache in your image is always active.