Skip to main content

Instant Kubernetes Is 5-10 Minutes, Not Sub-Second: The Real Cost Math of Cluster-per-PR Preview Environments

9 min readDora NodaDora Noda
Share
On this page

A 2026 Show HN promised "instant Kubernetes provisioning with KVM and gRPC" — and the demo is worth your time. Then read the author's own numbers: KVM VMs spin up in 30–60 seconds, and a production-ready cluster lands in 5–10 minutes. That is a solid platform-engineering result. It is also 10 to 60 times slower than "instant," and the gap between the headline and the budget matters, because a self-hosted PaaS that takes the headline literally will design its preview environments around a primitive that doesn't exist.

Here is the verdict up front: at fleet scale there is no sub-second full cluster, only different places to pay the minutes. The table below scores the five real preview strategies on what each one costs per pull request and what breaks first at dozens of concurrent PRs. The rest of this post is the evidence — including a line-item budget of where the minutes actually go.

StrategySpin-up latencyMarginal cost per PRIsolationBreaks first at
Full cluster per PR (CAPH / RunOS-style)5–20 min3+ nodes × PR lifetime in node-hoursFull: own control plane, own CNIControl-plane sprawl × N; cold image caches on every node
Namespace per PRSecondsMillicores of quota on shared nodesSoft: shared control plane + CNINoisy neighbors; cluster-scoped resources (CRDs, webhooks) collide
vCluster per PRSecondsA namespace + a small virtual control planeMedium: virtual API server, synced workloadsSyncer edge cases; host-cluster etcd still shared
Pre-warmed poolSeconds (to claim)Idle burn of the whole pool, 24/7Depends on pool unit (node, cluster)Pool sizing: too small queues PRs, too big burns cash
Long-lived stagingZero (already up)One environment, always onNone between PRsSerializes all PRs; "works on staging" stops meaning anything

The short version: namespace-per-PR is the default you should have to argue your way out of. Everything else buys lower latency or stronger isolation with a cost curve that only makes sense past a specific scale — and this post names each curve.

What the demo actually built (credit where due)

The project is RunOS, and the engineering is genuinely interesting. Two agent types: server agents on VM hosts talking to the backend over gRPC bidirectional streams, node agents on each Kubernetes node handling cluster ops and service installs. The key insight is architectural, not performance: agent-initiated streams mean the agents dial out, so there is no firewall configuration and no public IP requirement on the VM hosts. Provisioning is KVM via libvirt's Go bindings on Ubuntu Cloud 24.04, bootstrap is kubeadm plus Cilium, the mesh is WireGuard managed at the OS level rather than the Kubernetes level (so nodes stay reachable even when Kubernetes itself is broken), storage is OpenEBS plus Longhorn, and one-click installs cover twenty-plus services from PostgreSQL and Kafka to Ollama and Grafana.

The KVM choice is defended the right way — boring technology on purpose: battle-tested, solid Go bindings, GPU passthrough for AI workloads, good isolation-per-cost. None of my skepticism below is about this stack. It is a credible way to run self-hosted Kubernetes with turnkey data services, and "zero to working cluster with Postgres, Kafka, and monitoring in under ten minutes" is a real achievement. The problem is only the word "instant" and what a platform team might architect on top of it.

Where the minutes actually go

A full cluster has an irreducible time budget, and every line item is load-bearing:

  • VM provisioning: 30–60 seconds. This is RunOS's own fast path — KVM guests from cloud images on ready hosts. It assumes the host has capacity. On bare metal via Cluster API, the equivalent step is measured in minutes: CAPH's own MachineHealthCheck examples budget a 20-minute node-startup timeout, and its e2e logs routinely show hosts still provisioning several minutes in.
  • Kubernetes bootstrap: 1–3 minutes. kubeadm init, CNI installation (Cilium agents must come up on every node before pods schedule cleanly), WireGuard key exchange across the mesh.
  • Storage and services: 2–5 minutes. OpenEBS/Longhorn initialization, then whatever data services the preview needs — a Postgres cluster alone needs its own init, backup-sidecar wiring, and readiness gates.
  • Control-plane HA tax (managed offerings): 10–20 minutes. Practitioner reports consistently put EKS control-plane provisioning at 10–20 minutes for a multi-AZ HA API server. Self-hosted kubeadm skips this by running a single control-plane node — which is fine for a preview, until you need the preview to tell you anything about HA behavior.

Add it up and 5–10 minutes is the floor for an honest full cluster, not the ceiling. Sub-second would require skipping VM boot, bootstrap, CNI, and storage entirely — i.e., restoring a snapshot rather than provisioning a cluster, which is a different primitive (Kedge's 3ms microVM fork lives at that end of the spectrum, with snapshot-tree constraints of its own). Anyone selling "instant clusters" is either snapshotting or redefining "cluster."

The fleet-scale math: dozens of concurrent PRs

One demo cluster hides every cost that matters. Run the numbers for a team with 30 open PRs, each needing a 3-node preview for an average of 3 days:

  • Node-hours: 30 PRs × 3 nodes × 72 hours ≈ 6,500 node-hours per cycle — burned while developers sleep, review, and argue in comments. Multiply by your per-node-hour rate; on owned Hetzner hardware that rate is low per hour but the capacity is finite, which is worse — 90 nodes of preview consumes machines your tenants' production workloads could be using.
  • Cold image caches: every fresh node pulls every image cold. At fleet scale the registry becomes the bottleneck and PR spin-up time degrades exactly when the most developers are waiting — Monday morning, all 30 PRs rebased, all caches cold simultaneously.
  • Per-cluster overhead × N: each full cluster carries its own etcd, API server, Cilium control plane, and monitoring scrape targets. Thirty control planes for thirty previews is thirty sources of pager noise, thirty certificate rotations, thirty things to garbage-collect when the PR merges — and teardown that misses (the webhook that never fires) leaves full clusters, not stray namespaces.
  • CNI bring-up per cluster: Cilium agent startup, identity allocation, and policy computation repeat from zero each time instead of amortizing across tenants on shared nodes.

Sensitivity check: at 3 concurrent PRs, none of this matters and cluster-per-PR is simplest to reason about. Past roughly 15–20 concurrent previews, the overhead lines cross the namespace lines, and the team running full clusters starts building — on purpose, over months — the exact shared machinery (pools, caches, quotas) that namespaces would have given them for free.

What to run instead

Namespace per PR is the default. Seconds to create, quota-capped, torn down by TTL, with network policies for tenant separation. Our namespace-per-PR recipe covers the ArgoCD ApplicationSet mechanics and the cost curve at 10 versus 50 concurrent PRs. Argue your way out of it only with a specific requirement below.

vCluster per PR when you need cluster-scoped APIs. If previews install CRDs, mutating webhooks, or operators that fight over shared cluster state, a virtual cluster — deployed in seconds inside a host namespace, per vCluster's own docs — gives each PR its own API server without its own machines. You keep the seconds-scale spin-up and pay only a small virtual control plane per PR. The failure mode to respect: the syncer and the shared host etcd are still shared fate.

Pre-warmed pool when PR latency must be seconds and isolation must be hard. Keep N booted clusters (or nodes) warm and claim-on-PR. The math to get right is pool sizing against your PR arrival rate — too small and PRs queue behind each other (worse than slow provisioning: unbounded latency), too big and the idle burn dwarfs what provisioning ever cost. Size from your merge queue's p95, not its average.

Full cluster per PR only for platform testing. Conformance runs, CNI upgrades, Kubernetes minor-version validation, multi-tenant isolation tests — workloads where the cluster itself is the subject. For app previews it is the most expensive way to learn what a namespace would have told you.

Watch snapshot-fork. Kedge-style restore-from-snapshot is the only approach that honestly attacks the time budget rather than rearranging it, and it may eventually collapse this whole table. Today it trades boot time for snapshot-tree complexity (warm pools by another name, plus CoW storage semantics to operate). Revisit yearly, not quarterly.

The takeaway for a self-hosted PaaS

RunOS deserves credit for the right architecture — agent-initiated gRPC orchestration, boring KVM, OS-level WireGuard — and its 5–10 minutes is genuinely good for what it builds. But "instant" did real work in that headline: it smuggled in a premise (clusters appear in under a second) that would justify deleting pools, headroom, and all the unglamorous capacity machinery a fleet actually runs on. The minutes are load-bearing. Design preview environments around where the minutes go — namespaces by default, virtual clusters for API isolation, pools only with sizing math — and let full clusters stay what they are: the thing you test the platform on, not the thing you test every pull request on.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex