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.
| Step | What happens |
|---|---|
| t+0ms | Request arrives at the ingress; the service is at 0 replicas → routed to the activator. |
| buffer | The activator holds the request and signals the autoscaler to scale 0→1. |
| start | A pod is scheduled; the Node standalone server boots — V8 reads its bytecode cache. |
| serve | The 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: 20Every 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:
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 fasttargetBurstCapacitycontrols whether a buffer sits in front of your pods while they scale up. Set it to-1to 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.panicWindowPercentageandpanicThresholdPercentagecontrol how quickly the autoscaler notices a burst and reacts. A smallerpanicWindowPercentage(as low as1) makes it look at a shorter, more recent slice of traffic; a lowerpanicThresholdPercentage(as low as110) 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.