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, scaleDownDelay, 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,
  },
};

Keeping the last pod for a while after traffic stops

scaleDownDelay keeps the last pod routable for a window after the traffic stops, then scales to zero as usual:

spec:
  scaling:
    minScale: 0
    maxScale: 10
    scaleDownDelay: 5m   # a duration: 30s, 5m, 1h

A request inside the window is served by a pod that never went away — measured under 100 ms, against seconds for the same request from zero — and it cannot hit the multi-second stall a request can otherwise suffer when it arrives exactly while a pod is being taken out of rotation. Outside the window nothing changes: the pod goes, and the next request pays a normal cold start.

The cost is one idle pod for the length of the window after every burst, including the database connections that pod holds open. If you have sized maxScale × poolMax against your database's connection budget, this does not raise the peak — but it does keep idle connections occupied for longer, so count them.

New apps are scaffolded with scaleDownDelay: "5m" in their kn-next.config.ts. Delete that line for immediate scale-to-zero; shorten or lengthen it to move the trade. Existing apps are unaffected until you add the field yourself.

Your Knative installation has to honour it. knext asks the platform to hold the pod; an installation that ignores or clamps the request will scale down as it always did, and the accepted range is the installed platform's, not knext's. kn-next doctor reports the version you are running against. The knext operator has to know the setting too: if the operator on your cluster predates it, kn-next deploy stops at its preflight check and names the field — update the operator first, then deploy. The delay also covers the app only — a database that sleeps when idle keeps its own wake time, so the first query after an idle period can still be slow on a warm pod. If your database runs on a scale-to-zero engine, keep its idle window at least as long as this delay — otherwise the warm pod answers instantly and then waits for the database to wake anyway. That idle window is a platform setting your cluster operator owns, not something configured per app.

The cold-start budget

A cold start is pod scheduling + container start + the server boot + first-request work. Only the last two are knext's to shave; the first two belong to the platform. The boot is where a Next.js app otherwise spends real CPU compiling JavaScript — so knext attacks exactly that by compiling at build time: the default target is a compiled single executable whose whole server bundle is precompiled to bytecode (measured 61 ms median process boot against the Node standalone's 884 ms); on the legacy Node standalone shape, a V8 compile cache baked into the image does the same job — see bytecode caching.

Be clear about where that win lands, though: on a real Knative cluster the pod scheduling and container start dominate the end-to-end budget — measured on the order of ~3.4–3.6 s from zero replicas to first response, comparable across the compiled and Node targets, because the platform overhead swamps the boot delta. The boot optimization is real and cuts the part knext owns, but the largest lever on the total number is the platform tier — keeping a warm window after a burst (below), min-scale, activator capacity, image pre-pull, and probe tuning.

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 — measured anywhere from ~1.3 s on a lightly-loaded cluster to ~3.4–3.6 s on a busy one — is dominated by pod scheduling, not the server boot, so shaving the boot helps but does not make scheduling free. A second reason a fast boot can't fully show through: the readiness probe polls on an interval, so a sub-second boot is only observed at the next probe tick — the probe cadence is itself a floor under the boot win. 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