Kubernetes Swap Packs Up to 3x More Sandboxes per Node
Headless-browser agents spend most of their lives waiting for the next prompt, and Kubernetes node swap now lets that idle memory move to disk. On October 5 the Kubernetes blog published benchmarks for it, including a node that went from 80 to 240 concurrent sandboxes, a 3x gain. Those are the authors' numbers from one Google Cloud setup, so treat them as a target to test, not a promise.
What the Kubernetes node swap post reports
The Kubernetes blog post by Ocean Xie and Yuan Wang says node swap reached general availability in v1.34. It targets a familiar waste: agent sandboxes need a lot of memory to start and run untrusted code, then sit idle while still holding it. That caps how many pods fit on a node.
Swap was discouraged for two reasons, per the post. Under cgroup v1, memory and swap shared one combined limit, and paging to slow spinning disks hurt. Swap support now relies on cgroup v2, which tracks swap separately, and fast NVMe Local SSDs largely remove the disk penalty.
The authors' results, all with Local SSD swap:
| Workload | Without swap | With swap | |---|---|---| | Python sandbox, gVisor | 80 sessions | 240 sessions | | Headless Chrome, gVisor | 80 pods | 160 pods | | Headless Chrome, Kata Containers | 40 pods | 50 pods | | Headless Chrome, plain runc (c4-standard-32, 120 GB) | 512 pods | 768 pods | | Linux kernel build | 600 MB limit | 300 MB limit |
The 3x headline is the Python test. The browser gains run from 1.25x to 2x, and Kata stopped at 50 pods because CPU saturated.
On latency, the authors say that at peak density the per-pod slowdown comes mainly from pods competing for CPU, not from swap I/O. They suggest running below peak if you have a latency target. The kernel build is the warning: at a 300 MB limit it finished in 374s versus 433s at 600 MB, but at 200 MB the working set spilled into swap and runtime rose over 40%. Swap is insurance for bursts, not a replacement for active RAM.
Why it matters for ad-check and QA browser agents
The post doesn't test ad verification, but headless Chrome sandboxes have the same shape: a memory burst while a page loads, then a long wait. If your marketing team runs agents that open landing pages, check ad creatives or run QA flows, much of your node memory is probably idle. The post calls memory often the first hard limit a cluster hits, so more sandboxes per node means fewer nodes.
There is a deadline attached. A second post on October 6 says that from v1.35 failCgroupV1 defaults to true, so the kubelet won't start on a cgroup v1 node, and the v1 fallback is scheduled for removal in v1.38. The swap prerequisite is also your upgrade homework.
Try Kubernetes node swap on one node pool
Confirm the node runs cgroup v2
stat -fc %T /sys/fs/cgroup/
The output cgroup2fs means cgroup v2 is enabled. Anything else, fix that first.
Enable LimitedSwap in the kubelet
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
failSwapOn: false
memorySwap:
swapBehavior: LimitedSwap
failSwapOn: false lets the kubelet start on a node with swap. The default NoSwap keeps pods off it, and LimitedSwap lets them use it. Provision swap first, then restart the kubelet so it detects it. The swap docs encourage encrypted swap on a dedicated disk, not a partition on the root filesystem.
Leave room between request and limit
apiVersion: v1
kind: Pod
metadata:
name: ad-check-browser
spec:
tolerations:
- key: gke-swap
operator: Equal
value: enabled
effect: NoSchedule
containers:
- name: chrome
image: <your-headless-chrome-image>
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 1Gi
Only Burstable pods get swap, meaning the memory limit is above the request. A container's share is its memory request divided by node memory, times the swap available to pods. Request equal to limit means no swap. The toleration matches the taint in the next step.
On GKE, use the managed setting
linuxConfig:
swapConfig:
enabled: true
Save that as system-config.yaml, then create a tainted pool:
gcloud container node-pools create <nodepool-name> \
--cluster=<cluster-name> \
--location=<location> \
--node-version=<1.34.1-gke.1341000-or-later> \
--system-config-from-file=system-config.yaml \
--machine-type=<machine-type-with-local-ssd>
The GKE docs require 1.34.1-gke.1341000 or later and a machine type that supports Local SSD (the default e2-medium doesn't). First- and second-generation series also need --ephemeral-storage-local-ssd. Swap is encrypted by default. Taint the pool, for example gke-swap=enabled:NoSchedule, as the docs suggest.
Measure before you pack
kubectl top nodes --show-swap
kubectl top pod --show-swap
Start below the post's peak counts and raise density until page-load time breaks your target.
Gotchas before you pack nodes tighter
- The scheduler doesn't account for swap, so taint swap nodes or non-swap workloads will land there.
- Guaranteed, BestEffort and high-priority pods get no swap.
- Throttled cloud disks are much worse than SSD or NVMe, and the post's numbers used Local SSD.
- A memory-hungry pod can force its neighbours into swap.
- The docs recommend running control plane nodes without swap.
For anyone running browser agents in production, swap turns idle memory into capacity, provided you verify cgroup v2, limit it to Burstable pods and measure latency on your own hardware.