The turns of every Claude Code, Codex and OpenCode agent land on the fleet, however the agent was created. Two kinds of agent do not. An agent whose harness is xpander runs on the execution tier (
agent-worker), and on a workspace pod when it uses one. An SDK agent runs its loop in your own process.
Base footprint
Hybrid
The installer shows the same minimums, published at charts.xpander.ai/requirements.json.
Size for tasks running concurrently at peak, whatever triggers them. In xpander cloud production only ~5% of executions are interactive chat. The rest come from schedules, webhooks, the API and other agents.
Capacity includes ~50% headroom over pod requests for bursts, system pods and node upgrades. Capacity and storage cover the base services and workspaces. Add the executor fleet on top: each executor requests 2 vCPU / 8 GiB and has a 500 GiB volume (see Capacity Planning).
Sizing calculator
Enter peak concurrent tasks, agents, and how many use a workspace.How the estimate works
How the estimate works
The calculator applies the same model we use to size xpander cloud:
- Concurrent tasks is the primary driver. A task is any agent execution, from any source: chat, API calls, schedules, webhooks, MCP clients, Slack, or another agent. If you don’t know your peak concurrency yet, assume one concurrent task per agent - that covers most production organizations we measured. In practice most teams peak at just a few concurrent tasks (typically under 5), and even the busiest organization peaks around 30.
- Execution replicas (
agent-worker) = concurrent tasks ÷ 4 (each replica processes 4 tasks by default, configurable withMAX_CONCURRENT_EXECUTIONS). Each replica requests 2 vCPU / 2.5 GiB. - Agent Controllers: 1 replica up to ~8 concurrent tasks, 2 up to ~20, 3 beyond. Each requests 1 vCPU / 2 GiB.
- Active workspaces = concurrent tasks × workspace share, capped at the agent count. Budget ~0.5 vCPU / 1 GiB per active workspace. That is the measured typical load, and each can burst to its 2 vCPU / 3 GiB limit.
- Fixed base (~3 vCPU / 8 GiB): AI Gateway, MCP, Chat, Code Runner, AWS Operator, API, Redis, PostgreSQL, container registry and metrics-server at one replica each. Add the LLM Gateway’s two pods (0.5 vCPU / 1 GiB requested each).
- Cluster capacity = requests × 1.5 for burst, DaemonSets, and safe node drains. Storage = 48 GiB of chart volumes + 5 GiB per agent (an upper bound for workspace volumes; only an agent whose harness is xpander and that uses a workspace keeps one) + image cache per node.
- Executor fleet, where Claude Code, Codex and OpenCode turns run, is not in the estimate. Each executor requests 2 vCPU / 8 GiB and has a 500 GiB volume. Add it from Capacity Planning.
Per-service requirements
Per-service requirements
Per-replica values. Defaults are what the Helm chart applies out of the box. The recommended values raise the ones we found low against measured production usage.
Override any of these in your Helm values (keys are camelCase):
Air-Gapped
The full prerequisite list (network, license files, registry, tooling) is on Air-Gapped: Prerequisites.
Sizing tiers
Air-Gapped runs everything in-cluster: the execution tier (agent-worker) plus the control plane, the web UI, the Supabase stack (its own PostgreSQL 15, auth, storage, edge functions) and MongoDB.
Storage breakdown at the production tier:
All are RWO, on one StorageClass. The chart’s preflight verifies it exists.
External data tier. You can bring managed stores instead (Data Tier). A reference customer install uses:
- RDS PostgreSQL 16 on
db.t4g.smallwith 50 GiB gp3 - MemoryDB on
db.t4g.smallwith one shard and one replica - S3 as the Supabase Storage backend for blobs
agent-controller, agent-worker, api, agents and functions at ×2.
Scaling with usage
The chart ships fixed replica counts and no autoscalers. Scaling policy is yours:
Node capacity is the real ceiling, because workspaces and code-execution Jobs create pods on demand. Two postures:
- Static capacity: size the node group for peak and leave it fixed. Simplest, fully offline, recommended for sealed networks.
- Cluster Autoscaler: adds nodes when pods can’t schedule. It works in a no-internet VPC if your cloud’s scaling API is reachable (on AWS, the
autoscalingandec2VPC endpoints).
Agent workspaces
Each agent whose harness is xpander and that uses a workspace gets its own pod, created on demand and reclaimed when idle. Size for concurrently active workspaces, not total agents.
Idle workspaces use ~50 Mi. An active one uses ~0.5-1 vCPU and up to 1 GiB, bursting to its limits. The volume persists after the pod is reclaimed, so plan 5 GiB per workspace-using agent, not per pod.
The workspace image is ~1 GiB and is pulled on the first start on each node. Budget ~2 GiB of node disk for the image cache.
The fleet is always rendered by both charts (there is no enable flag). It starts warm with one executor (
fleet.replicas: 1 on Hybrid, global.fleet.replicas: 1 on Air-Gapped). Harness agents (Claude Code, Codex, OpenCode) run only on the executors, never as a pod per agent. So the chart always keeps at least one: replicas: 0 (or a blank value) renders one executor. The one case that renders zero is a live cluster whose executor volume can never bind (see Idle guard). Agent turns then wait in the queue until it is fixed.The executor fleet
The fleet is afleet-executor StatefulSet. Each replica is one PodSecurity baseline pod over one XFS volume: no privileged container, no host namespaces, no user namespaces and no node preparation. The pod holds the executor container, which claims turns, plus one slot-<i> container per turn slot (fleet.turnsMax). Each turn runs in its own slot container, as the agent’s own uid. It has its own processes, /proc, /tmp and memory and CPU limit, behind a seccomp profile, no_new_privs and Landlock. An agent’s directory lives on exactly one executor’s volume, and its turns always run there.
Per executor
Per turn
Ceilings per turn (fleet.turn). An agent can lower its own, never raise it.
Concurrency caps
There is no per-organization cap.
fleet.orgMaxTurns is removed, and a license’s agent cap does not limit concurrent turns. A values file that still sets it upgrades cleanly, and the key renders nothing.
Reading the runtime logs
One turn shows in two logs, the executor and the Agent Controller:fleet-executor-<n>, container executor), in order:
Agent Controller (
deploy/agent-controller). The same turn shows as:
direct turn conversation=<id> task=<id> status=... received_to_enqueued_ms=... enqueued_to_claimed_ms=...: the message became a task and was picked up by the fleet.enqueued_to_claimed_msis where a cold executor shows. The turn runs on the model the conversation picked. The agent record’s model is only the default for new conversations.event=fleet.claimed executor=fleet-executor-0: which executor took it.event=compute_usage.emitted cli=claude-code cpu_seconds=...: the compute record written when the turn ended.
llm-gw, kubectl -n xpander logs -f -l app=llm-gw). Each model call the turn made is one event=llm_gw.call llm_route=gateway_direct status=... org=<id> agent=<id> task=<id> client=... provider=... request_id=<id> line. To see the model a call used, paste its request_id into Request or trace ID and press Enter in Platform > AI governance > Logs, on your install’s own address.
Node prerequisites
All required: a cluster that fails one runs no agent turns.install.sh and the Air-Gapped preflight check each and name the fix. There is no Kubernetes 1.33, kernel 6.3, containerd 2.0 or user.max_user_namespaces minimum, and no node configuration.
Landlock on the executor nodes
Landlock limits each turn to the agent’s own files. It is on by default (fleet.landlock.enabled: true, or global.fleet.landlock.enabled on Air-Gapped). On a node without it, the executor takes no turn: /health answers 503, and its refusal field and the log say the kernel has no Landlock.
On GKE, keep the cluster on Container-Optimized OS and add one Ubuntu node pool for the executors:
fleet.landlock.enabled: false, and Landlock is off on every node.
/health then shows landlock_enabled: false.
Fleet egress
The executors’ NetworkPolicy (fleet.networkPolicy.enabled, default true) gives agents the same reachability as any other pod, private ranges and in-cluster services included, minus link-local. fleet.networkPolicy.exceptCidrs defaults to ["169.254.0.0/16"] and exceptCidrsV6 to ["fe80::/10"]. Setting either replaces the whole list, so keep the link-local range in it. To close more ranges, use extraExceptCidrs. extraEgress appends raw egress rules. fleet.egress.allowHosts is still accepted for older agent-harness images and is no longer needed.
Autoscaling
An agent runs only on the executor that holds its home, so scaling in would strand agents. Withfleet.keda.enabled: true the chart renders a KEDA ScaledObject that keeps every executor running:
One executor per node, so grow the node group and
maxReplicas together. Without KEDA (the default), set fleet.replicas. On a sealed network, a fixed count sized for peak is recommended.
Safe rollout, drain, and upgrades
A roll never kills a running turn
A rolling update, a KEDA scale-in and a pod delete all sendSIGTERM, which the executor treats as a drain, not a kill:
- The executor stops claiming work, and its heartbeat says
draining. The Agent Controller routes around it: nothing new lands on it, and whatever was queued for it goes back to the shared queue. It still counts as fleet presence, so new turns wait for the replacement instead of starting anywhere else. - The turns in flight keep running for up to
fleet.executor.drainSeconds(3600 s by default). A turn still running when that window ends is stopped the way a user stop is. The CLI is interrupted, its session is kept, and the task endsstoppedwith “The executor is restarting; send the message again to continue where it left off.” Sending the message again resumes the session on the new pod. No task is left hanging as executing. - Each slot container holds its turn until the executor’s health endpoint reports draining with no turn in flight. The pod grace (
fleet.executor.terminationGraceSeconds, 7500 s: the CLI run budget plus five minutes) is a ceiling, never a wait. The pod leaves as soon as its containers exit.
- If your turns run long, raise
drainSecondstoward the run budget. It must stay at least 120 s under the pod grace. The Air-Gapped chart refuses to render otherwise. - Never shorten a termination with
--grace-period=0or--forceon a pod that still has turns. That is the one way to kill a turn. - Plan image rolls for quiet hours when you can. They are safe at any time, only slower under load.
- The drain needs an
agent-harnessimage that carries it. An older executor ignores the setting and exits as before.
Idle guard when the StorageClass is missing
The executor is Ready only when its volume binds on an XFS +prjquota StorageClass. So an install never waits on one that cannot bind, both charts check the live cluster:
helm template and dry-runs always render the configured replicas (at least one). fleet.idleGuard: "off" removes the lookup for Argo CD, Flux and policy gates, and the executor stays Pending until its class exists. Create the class (or point provisioner at your CSI driver) and the next upgrade brings it up. Meanwhile turns queue.
--wait, --atomic and the installer
helm upgrade --wait (and --atomic) waits for the executor StatefulSet:
- A running turn holds the roll. With
--wait, an upgrade during a long turn can take up to the drain window plus pod start. Give--timeoutat least the drain window (3600 s by default) or upgrade in a quiet period. - An idle executor never stalls an upgrade. With the idle guard above, a cluster without the storage prerequisites renders the executor at zero replicas and the upgrade completes.
install.sh handles both for you:
Capacity planning
Plan in memory-backed executors: 10 slots on a 16 vCPU / 128 GiB node (for exampler6i.4xlarge), with executor memory raised so each turn’s 8 GiB is backed. The rest covers the kubelet, DaemonSets, the 4 GiB /dev/shm and the 20 GiB package cache.
- Slots, not agents. One executor serves many idle agents but runs at most
turnsMaxturns at once. Extra turns queue. - Agents per volume is a ceiling. Most directories stay far below the quota, and clones share one object store per repository, so a 500 GiB volume holds far more than 25 agents in practice.
- Memory headroom. Keep the executor’s limit at or below ~80 % of the node, and
turnsMax x 8 GiBwithin what the node can back. - Smaller executors are fine. On the defaults (8 vCPU / 28 GiB, 12 slots) one fits on a 16 vCPU / 32 GiB node, with the risk in the warning above. Lower
turnsMaxto 3 or 4 for heavy builds. - Workspace pods are separate: 0.25 vCPU / 256 Mi request, up to 2 vCPU / 3 GiB while active. See Agent workspaces.
Data services and storage
The chart deploys single-replica Redis and PostgreSQL StatefulSets and a container registry alongside xpander:
Baseline storage: 48 GiB of persistent volumes plus the 500 GiB agent volume per executor, plus the workspace volume of each workspace-using agent (see above). All volumes use your cluster’s default StorageClass unless overridden.
Production checklist:
- Node types: x86_64 only. General-purpose instances with a 1:4 vCPU:GiB ratio (
m5,m6i) fit the workload profile.t3burstable instances are fine for evaluation, not for production. - Raise the low defaults: API memory (table above) before real load.
- Enable autoscaling for the execution tier (
agent-worker) once concurrency grows beyond a couple of replicas. - Keep headroom: pod requests should stay under ~65% of cluster capacity so node upgrades and bursts don’t evict workloads.
- Storage class: SSD-backed volumes for Redis and PostgreSQL.
Image and registry
The executor and every slot use this image. Budget the ~1.5 GB pull per executor node. On Air-Gapped, mirror it at the pinned tag before the first executor starts.
Network egress
Executors hold no LLM provider key. A turn’s model calls go straight to the in-cluster LLM Gateway, which is always on, with the run’s own token. The gateway is the component that egresses to the model provider.
The executor refuses to register while the instance metadata endpoint (
169.254.169.254) answers from the pod, so a turn can never read node credentials.
Model providers
Keys are saved in the organization (Platform > AI governance > Models) or on the install (secrets.static), and the LLM Gateway adds them to each call. Per-provider keys, Bedrock authentication and default harnesses: AI vendors.
Air-Gapped vs Hybrid
Next steps
Install Hybrid
The Hybrid installer.
Install Air-Gapped
The Air-Gapped installer.
Set up an EKS cluster
Nodes and StorageClasses.
Understand two-file licensing
Seat and agent caps.

