Packing More Agents Onto One Node: The GKE Agent Sandbox Numbers
Google Cloud published an experiment that answers a question most agent platforms hit in month two: how many agents actually fit on the box you already pay for. The setup was one n2-standard-48 node running a fleet of OpenClaw agents, and the variable was isolation and orchestration rather than model choice. With microVM isolation the workload hit a wall at 61 agents before health checks started failing. Swapping to GKE Agent Sandbox and gVisor took it to 88. Adding pod snapshots so idle agents could be frozen took it to 133 with warm pools, and 274 in an aggressively oversubscribed configuration. Two steps, big numbers, and both of them are things you can copy without changing your agent code.
The whole experiment fits on one node. The interesting part is what changes between steps.
Why agent workloads break naive capacity planning
Google's framing is the useful part. Agents tend to operate in bursts: they process requests or execute code for a while, then sit idle waiting for a user or an external trigger. Provision statically and you are paying CPU and memory for agents that are doing nothing. The baseline in their test was the common answer to that problem, which is putting each agent in its own microVM, for example a Kata container, to get hardware-level isolation. That gives you a real security boundary and an immediate scaling wall, because every microVM carries its own guest operating system. Memory and CPU go to guest kernels instead of agents. At 61 agents on the node, reliability dropped and health checks began failing regularly. That is the honest shape of the problem: the security choice was correct and the density was terrible.
# Encode the isolation guarantee in a spec, not in a convention someone remembers.
import json
def sandbox_spec(name, runtime_class="gvisor", cpu="2", memory="4Gi"):
return {
"apiVersion": "agents.x-k8s.io/v1alpha1",
"kind": "Sandbox",
"metadata": {"name": name, "labels": {"runtime": runtime_class}},
"spec": {
"runtimeClassName": runtime_class,
"replicas": 1,
"podTemplate": {
"spec": {
"containers": [{
"name": "agent",
"resources": {
"requests": {"cpu": cpu, "memory": memory},
"limits": {"cpu": cpu, "memory": memory},
},
}],
"automountServiceAccountToken": False,
}
},
},
}
EGRESS = [] # default deny. Add destinations one at a time, with a reason each.
print(json.dumps(sandbox_spec("research-agent-01"), indent=2)[:430])
print("explicit egress rules:", EGRESS or "none, so nothing gets out")
# The isolation claim lives in runtimeClassName. Everything else is scheduling detail.Swapping microVMs for gVisor sandboxes
The first optimisation was moving the same workload from microVMs to GKE Agent Sandbox, a Kubernetes primitive built for agent code execution and computer use. Instead of a full guest operating system per agent, it uses gVisor, which runs a user-space kernel called the Sentry to intercept and filter system calls. You keep production-grade isolation for untrusted code and shed the guest-OS overhead. In Google's test that moved the ceiling from 61 to 88 agents on the same VM, a 44% increase, or as they put it, more than 40% more agents per vCPU and more than 30% lower cost per agent at a similar performance profile. GKE Agent Sandbox reached general availability in May, and Google notes usage grew more than 7x in under four weeks. Worth knowing: gVisor is the same isolation technology used to secure Gemini, so this is not a hobbyist tier.
# Google's measured points on one n2-standard-48 node, turned into something reusable.
MEASURED = {
"microvm_kata": {"agents": 61, "note": "baseline: health checks start failing"},
"gvisor_sandbox": {"agents": 88, "note": "+44% on the same fixed node"},
"warm_pool": {"agents": 133, "note": "performance-optimised, sub-second start"},
"oversubscribed": {"agents": 274, "note": "cost-optimised, start under 5s"},
}
def density(base, target, node_vcpus=48):
b = MEASURED[base]["agents"]
t = MEASURED[target]["agents"]
return {
"from": base, "to": target,
"agents_per_vcpu": round(t / node_vcpus, 2),
"density_multiplier": round(t / b, 2),
"cost_per_agent_index": round(b / t, 3),
}
for pair in [("microvm_kata", "gvisor_sandbox"),
("microvm_kata", "warm_pool"),
("microvm_kata", "oversubscribed")]:
print(density(*pair))
# Google summarises the combination as more than 3x density and up to 75% lower cost per agent.
# Sizing should use your own measured agent counts, not someone else's multiplier.Sandboxing is a scheduling problem as much as a security problem
The second step is about idle agents, not busy ones
Sandboxing helps you pack active work more tightly. It does nothing for the agent that finished its task and is waiting for a webhook. That is what GKE Pod snapshots address: instead of leaving idle agents resident, you checkpoint them to persistent storage, which hands their CPU and memory back to the cluster, and a controller resumes them from the snapshot in milliseconds when a trigger arrives. This is oversubscription, and Google is unusually candid about the trade-off. Agents have different latency requirements, so a single cluster-wide strategy will either hurt user experience or waste money. Their answer is to run different policies side by side. In the performance-optimised configuration, with pre-warmed sandboxes held in a pool, they reached 133 agents. In the cost-optimised configuration, with heavy oversubscription and startup kept under five seconds, they reached 274 on the same node.
# Three latency classes, three different orchestration strategies.
CLASSES = {
"realtime_coding_assistant": {"startup_ms_max": 1000, "tolerates_queuing": False, "strategy": "warm_pool"},
"autonomous_teammate": {"startup_ms_max": 5000, "tolerates_queuing": True, "strategy": "snapshot_resume"},
"headless_background": {"startup_ms_max": 3600000, "tolerates_queuing": True, "strategy": "max_oversubscription"},
}
def classify(agent, startup_budget_ms, interactive):
for name, spec in CLASSES.items():
if startup_budget_ms <= spec["startup_ms_max"] and spec["tolerates_queuing"] >= (not interactive):
return {"agent": agent, "class": name, "strategy": spec["strategy"]}
return {"agent": agent, "class": "unclassified", "strategy": "warm_pool"}
for args in [("copilot-inline", 800, True),
("pr-reviewer", 4000, False),
("nightly-report", 300000, False)]:
print(classify(*args))
# Getting this wrong in either direction costs you: either latency, or over-provisioned nodes.Classify your agents before you size the node
The most practical output of the whole exercise is the three-way classification. A real-time coding assistant needs sub-second startup and cannot tolerate queueing, so it pairs pod snapshots with warm pools that are already running and isolated. An autonomous teammate can accept a few seconds of startup, so a plain suspend-and-resume policy is enough and it stops consuming compute while idle. A headless background agent, say a nightly research job, will happily wait an hour for capacity, so it gets maximum oversubscription. Google also names the failure mode that follows from getting this wrong: the thundering herd, where a surge of agents wakes at once and demands compute simultaneously. Their tunable answer is warm pools for guaranteed performance and suspend-and-resume for cost, and you choose per node pool rather than globally.
# Idle agents are the bill. Freeze them, and be explicit about the wake-up budget.
def lifecycle(state, idle_seconds, snapshot_after_s=120, wake_budget_ms=5000):
plan = {"state": state, "action": "noop", "frees_resources": False}
if state == "running" and idle_seconds > snapshot_after_s:
plan.update(action="checkpoint_to_persistent_storage", frees_resources=True)
elif state == "snapshotted":
plan.update(action="resume_on_event", frees_resources=False)
plan["wake_within_budget"] = wake_budget_ms <= 5000
return plan
for case in [("running", 30), ("running", 900), ("snapshotted", 0)]:
print(lifecycle(*case))
# A snapshot that takes too long to resume is just an outage with extra steps. Budget it.Freeze idle agents instead of paying for the pause
The numbers, and how much to trust them
Google's summary figures are more than 3x agent density and up to 75% lower cost per agent, and the concrete endpoints in their own table are 61 agents and 274 agents on the same node. Those two statements sit at slightly different angles, which is normal for a vendor blog, so treat the multipliers as directional and the agent counts as the data. What transfers cleanly is the ordering: the isolation change is worth roughly a third on cost with no change to your workload, and the orchestration change is worth several times more, but only for agents that are genuinely idle most of the time. If your agents run hot continuously, oversubscription buys you nothing and snapshot overhead costs you latency. Measure your own duty cycle first, because that ratio is the one that decides which configuration is right.
# Cost per agent is the figure that survives a budget review.
NODE_USD_PER_MONTH = 1168.0 # illustrative: one n2-standard-48 class node at list price
def cost_per_agent(agents_on_node, node_cost=NODE_USD_PER_MONTH):
return {
"agents_on_node": agents_on_node,
"cost_per_agent_usd": round(node_cost / agents_on_node, 2),
}
baseline = cost_per_agent(61)["cost_per_agent_usd"]
for name, agents in [("microVM baseline", 61), ("agent sandbox", 88),
("warm pool", 133), ("oversubscribed", 274)]:
row = cost_per_agent(agents)
row["config"] = name
row["reduction_vs_baseline"] = str(round((1 - row["cost_per_agent_usd"] / baseline) * 100, 1)) + "%"
print(row)
# Swap in your own node price and your own measured agent count. The shape of the curve is the point.What to do with this on your cluster
Three takeaways. Pick your isolation layer deliberately and write it into the spec, because gVisor gives you a user-space kernel boundary while a microVM gives you hardware-level isolation with guest-OS overhead, and the right answer depends on whether you are running untrusted model-generated code or your own. Classify agents by startup budget rather than by team, since the classification is what determines the orchestration policy. And put a number on cost per agent, because density conversations die in budget reviews while a figure like cost per agent per month survives. If your platform is at the microVM baseline with 61 agents on a node and a large share of them idle, the cheapest available win is not a bigger node.
📌 Frequently Asked Questions
What did Google actually measure?
They packed OpenClaw agents onto a single GKE node backed by one fixed n2-standard-48 VM instance, and progressively changed the isolation and orchestration strategy to see how many agents could run before reliability degraded.
How many agents fit on the baseline configuration?
With each agent in its own microVM for hardware-level isolation, the test hit a scaling wall at 61 agents, after which reliability dropped and workload health checks began failing regularly.
What did GKE Agent Sandbox change?
It replaced microVMs with gVisor-based sandboxes that use a user-space kernel instead of a full guest OS, raising the count from 61 to 88 agents, a 44% increase, with more than 40% more agents per vCPU and more than 30% lower cost per agent.
How does oversubscription reach 274 agents?
By checkpointing idle agents with GKE Pod snapshots, which releases their CPU and memory back to the cluster, and resuming them from the snapshot when a task trigger arrives. In a cost-optimised configuration with startup kept under five seconds, 274 agents ran on the same node.
Should I oversubscribe all my agents?
No. Oversubscription suits agents that are idle most of the time. Latency-sensitive agents such as a real-time coding assistant need warm pools and sub-second startup instead, otherwise you trade cost savings for user experience.