knext

Scale-to-zero & cold starts

How Knative drops idle knext services to zero, and how knext keeps the wake fast.

When a knext service is idle, Knative drops it to zero replicas — you pay nothing for traffic you're not serving. The first request after that wakes it through the activator. This page is how that works, and how knext keeps the wake fast.

How it scales to zero

Each service runs behind a queue-proxy that reports concurrency to the Knative Pod Autoscaler (KPA). After the stable window with no requests, the KPA scales the deployment to 0. Incoming requests then route to the shared activator, which buffers the request, triggers a scale-up, and forwards once a pod is ready.

StepWhat happens
t+0msRequest arrives at the ingress; the service is at 0 replicas → routed to the activator.
bufferThe activator holds the request and signals the autoscaler to scale 0→1.
startA pod is scheduled; the Node standalone server boots — V8 reads its bytecode cache.
serveThe activator forwards the buffered request; later requests hit the pod directly.

Set it per app

spec.scaling.minScale: 0 enables scale-to-zero; raise it to keep a warm floor for latency-critical services. The operator translates this to KPA annotations on the Knative Service.

Which value is right depends on the workload, not on the app. For a decision guide — when to stay at zero, when to schedule a floor over a known peak, and when a permanent replica is the only honest answer — see Warm floors by workload class.

spec:
  scaling:
    minScale: 0     # idle → zero pods
    maxScale: 20

Every scaling knob on this page — minScale, maxScale, containerConcurrency, poolMax, warmSchedule, targetBurstCapacity, panicWindowPercentage, panicThresholdPercentage — is also settable from your kn-next.config.ts scaling: block, so the app and its autoscaling are configured from one file:

kn-next.config.ts
const config: KnativeNextConfig = {
  name: 'acme',
  scaling: {
    minScale: 0,
    maxScale: 20,
    containerConcurrency: 20,
    targetBurstCapacity: -1,
  },
};

The cold-start budget

A cold start is pod scheduling + container start + the server boot + first-request work. The boot is where a Next.js app spends real CPU compiling JavaScript — so knext attacks exactly that with bytecode caching: on Node, NODE_COMPILE_CACHE on a persistent volume means each cold pod reads pre-compiled V8 bytecode instead of recompiling from source; on the Bun runtime, the standalone tree is precompiled to bytecode at build time (measured −47% boot, 287 ms → 152 ms median on a real Next.js 16.2 build).

Deprecated — no setup needed. The opt-in PVC-backed bytecode cache (spec.cache.enableBytecodeCache) described below is deprecated; the compile cache is now baked into your image by default, so it works from the first cold pod on any cluster with nothing to enable. See bytecode caching.

Cross-cold-start caching needs persistence. The (deprecated) volume-backed bytecode cache only helps across pods if it survives pod death — knext mounts it from a persistent volume, not pod-local disk, when spec.cache.enableBytecodeCache is set.

That volume is also a scaling limit. It attaches to one node at a time, so an app that scales out onto a second node will have those extra pods sit Pending. Bytecode caching is therefore opt-in: it trades horizontal fan-out for boot speed. Enable it on cold-start-sensitive zones that stay narrow; leave it off on anything that bursts wide. See bytecode caching.

Perspective on the numbers. On a real cluster, an end-to-end cold start (~1.3 s in a project benchmark) is dominated by pod scheduling, not the server boot — so shaving the boot helps but does not make scheduling free. Treat any specific millisecond figure as environment-dependent, not a guaranteed number — and a typical figure is not a worst case: cold starts vary a lot run to run, and an occasional wake can take several times the typical one. Size your warm floor against that tail, not the average. Node (with NODE_COMPILE_CACHE) remains the default runtime; Bun's build-time bytecode is the opt-in for squeezing the boot phase further.

Tuning burst response

Scale-to-zero and warm floors handle traffic you can predict. A burst you did not predict — traffic that jumps from a handful of requests to a spike faster than new pods can be scheduled — is a different problem, and spec.scaling has three knobs for it that work together:

spec:
  scaling:
    maxScale: 10
    containerConcurrency: 20
    poolMax: 5                       # keep maxScale × poolMax within your database's connection budget
    targetBurstCapacity: -1          # keep a request buffer in front of your pods during a scale-up
    panicWindowPercentage: 10        # react to a spike faster than the normal evaluation window
    panicThresholdPercentage: 200    # how far over target traffic has to go before reacting fast
  • targetBurstCapacity controls whether a buffer sits in front of your pods while they scale up. Set it to -1 to always keep that buffer in place — the safest setting for a bursty workload, at the cost of a small extra hop on every request. Set it to a specific number of requests to buffer only up to that amount. Leave it unset and the platform default applies.
  • panicWindowPercentage and panicThresholdPercentage control how quickly the autoscaler notices a burst and reacts. A smaller panicWindowPercentage (as low as 1) makes it look at a shorter, more recent slice of traffic; a lower panicThresholdPercentage (as low as 110) makes it react to a smaller overshoot above your target. Leave either unset and the platform default applies.

These knobs change timing, not the ceiling. targetBurstCapacity and the panic-reaction knobs control how a burst is absorbed while your service scales up — they don't change the maximum number of pods your service can reach (maxScale), and they don't change how many database connections each pod can open (poolMax, if you've set one). If you've sized maxScale and poolMax so their product fits your database's connection budget, tuning burst response for a faster reaction is safe. If you haven't set poolMax, tuning these knobs doesn't create a new risk, but it also doesn't protect you from the one that already exists — size maxScale and poolMax together first.

What a burst looks like in practice. In a representative benchmark, a sustained burst sized to saturate the pod cap (maxScale) scaled from zero to the cap in roughly ten to fifteen seconds, with essentially no errors. What the burst knobs (targetBurstCapacity, panicWindowPercentage, panicThresholdPercentage) reliably changed was how quickly capacity started being added, not how fast individual requests came back: with the buffered setting (targetBurstCapacity: -1) the service began adding pods immediately at the start of the burst, while the default setting took a few seconds to react. Response-time differences between the two settings did not hold up when the same comparison was repeated — treat the knobs as a lever on reaction speed, not on latency. They also didn't change the (already near-zero) error rate, and they don't change the tail, which stays dominated by cold start rather than by autoscaler reaction speed. Behavior like this is illustrative and environment-dependent, not a guarantee — see the perspective note above on cold-start numbers, which applies here too.

On this page