Google Kubernetes Engine (GKE) lets you scale Agent Substrate on GKE clusters to support high-concurrency AI agentic workloads while minimizing idle compute costs. By combining dedicated system infrastructure node pools with custom ComputeClasses and capacity buffers, you can isolate critical system components, automate hardware fallback across Compute Engine machine series, and keep standby Worker capacity warm.
Read this document to right-size your GKE cluster components, isolate system infrastructure and database node pools, configure automated node provisioning for WorkerPool resources, and plan for cluster maintenance and upgrades. This document is for platform administrators who provision and manage Agent Substrate on GKE infrastructure.
Before you begin
Before you configure your cluster infrastructure for scale, ensure that your environment meets the following requirements:
- GKE cluster mode and version: you must use a GKE Standard cluster in which both the control plane and the node pools run version 1.37 or later. GKE Autopilot clusters aren't supported for Agent Substrate on GKE.
- Agent Substrate on GKE installation: you must have an existing Agent Substrate on GKE installation or plan to install Agent Substrate on GKE with system infrastructure node isolation enabled. For installation steps, see Install Agent Substrate on GKE.
Right-size your Agent Substrate cluster
Size your cluster for peak concurrency and peak resume rates rather than averages. Worker nodes must hold the maximum number of Actors running at the same time, and the system infrastructure components and storage paths must keep up with the fastest rate of Actor resumes and suspends.
Estimate your workload peaks
If you don't have production measurements, then estimate your peak concurrency
and resume rate from your total registered Actor count (total_actors), the
average fraction of Actors running concurrently (average_active_fraction),
your traffic burst multiplier (burst_factor), and the average duration that an
Actor stays active per turn (average_seconds_active_per_resume):
peak_running_actors = total_actors × average_active_fraction × burst_factor
peak_resume_rate = peak_running_actors / average_seconds_active_per_resumeFor example, if you have 100,000 total Actors with 10% active on average (10,000 Actors) and a burst factor of two, then your cluster must support 20,000 peak running Actors. If each Actor runs for an average of 100 seconds before suspending, then sustaining that peak requires 200 resumes per second and 200 suspends per second.
Agent Substrate on GKE doesn't suspend idle Actors automatically; your application or orchestrator must trigger suspension.
Size the data plane
This section covers best practices for sizing the data plane, including Worker
Pods, GKE nodes, snapshot bandwidth, and the atelet node agent.
Calculate Worker Pods and GKE nodes
Each Worker Pod in a WorkerPool hosts multiple Actors. A Worker can accept a
new Actor while it hosts fewer Actors than the limit set by the --max-actors
flag on the ateworker container (default 1000) and has enough remaining CPU
and memory capacity to fit the ActorTemplate limits. For more information about
how Worker limits interact with Actor limits, see the open source Right-sizing
resources in the Agent Substrate API
guide.
Calculate the required number of Worker Pods with 20% available scheduling
capacity (the 0.8 factor):
slots_per_worker = min(max_actors, worker_memory / actor_memory, worker_cpu / actor_cpu)
total_workers = peak_running_actors / (slots_per_worker × 0.8)In this formula, the max_actors variable defaults to 1000 (configured by
the --max-actors flag). Subtract the memory that the Worker reserves for its
own process from the worker_memory value. For microVM WorkerPool resources,
add the per-Actor virtual machine monitor (VMM) memory overhead reported by the
Worker to the actor_memory value, because placement counts that overhead
against Worker capacity.
With one Worker Pod per node, the number of Workers equals the number of
GKE worker nodes. Don't allocate 100% of a node's allocatable CPU
and memory to the Worker Pod; leave capacity on each node for the atelet
DaemonSet (in the ate-system namespace) and GKE system daemons.
GKE Standard supports up to 65,000 nodes per cluster
and up to 512 Pods per node. The
default maximum is 110 Pods per node (configurable with the
--max-pods-per-node flag), and large clusters have additional
requirements.
Size snapshot bandwidth and node disks
Each Actor suspend writes a snapshot to the local node disk under the
/var/lib/ate directory path. In SYNCHRONOUS mode, the atelet agent uploads
the snapshot to Cloud Storage before the suspend operation returns. In
ASYNCHRONOUS mode, the atelet agent schedules the upload to
Cloud Storage after a delay of approximately the
--snapshot-upload-delay flag value (default 5m) from when the snapshot is
taken. Agent Substrate caches the latest snapshot on the node for
the period configured in the --snapshot-detach-delay flag, which defaults to a
value of 30m. An Actor that resumes within that window schedules onto the same
node and restores from the local disk instead of downloading the snapshot from
Cloud Storage.
Estimate your uncompressed snapshot size (snapshot_size) as the
ActorTemplate memory limit plus any data written to the writable root file
system or durable directories. Sparse file handling and zstd compression
reduce the compressed snapshot size (compressed_snapshot_size) transferred to
and from Cloud Storage, where Agent Substrate stores up
to two snapshots per Actor (the previous snapshot and the in-progress snapshot):
- Peak Cloud Storage bandwidth (each direction):
peak_resume_rate × compressed_snapshot_size - Peak node disk write throughput per node:
2 × peak_resume_rate × snapshot_size / total_nodes(both resume downloads and suspend checkpoints write to the node disk).
The default 100 GiB hyperdisk-balanced boot disk in GKE
provides 3,600 IOPS and 290 MiB/s of
throughput
(default performance scales with disk size), which can limit snapshot operations
under heavy guest I/O. When you create worker node pools, either mount a
dedicated disk at the /var/lib/ate/snapshots directory path or increase boot
disk performance by using the --boot-disk-provisioned-iops and
--boot-disk-provisioned-throughput flags with the
gcloud container node-pools create command.
Check Security Token Service quota
Each snapshot transfer to or from Cloud Storage requires a token scoped
to the Actor, which the ate-api-server component requests from the
Security Token Service (STS) API. The
ate-api-server component makes up to one STS request per Actor every 20 to 40
minutes (typically on the first resume in that window) and reuses the token
until 20 minutes before expiration.
The STS Exchange token global requests quota defaults to 6,000 requests per
project per minute and is shared with Workload Identity Federation for GKE:
- Required STS quota (requests per minute): estimate as up to 60 times
the peak rate of resumes for Actors whose last snapshot transfer was more
than 20 minutes ago, plus capacity for
Workload Identity Federation for GKE. If you don't know that rate, use
the
60 × peak_resume_rateformula as a fallback. For example, 200 resumes per second requires up to 12,000 requests per minute. - How to increase quota: request a quota increase on the Quotas & System Limits page in Google Cloud console. Snapshot transfers work without the Security Token Service API enabled in your project, but you must enable the API for the quota to appear on the Quotas & System Limits page. For more information, see Quota for the Exchange Token API in Security Token Service in the Workload Identity Federation for GKE documentation.
Tune the atelet node agent
The atelet DaemonSet runs one Pod per worker node to restore and checkpoint
Actors. In each atelet Pod, the objectstore-plugin container compresses and
transfers snapshots to and from Cloud Storage.
The following table describes key sizing settings for the atelet and
objectstore-plugin containers:
| Setting | Default | Sizing guidance |
|---|---|---|
atelet and objectstore-plugin container CPU
and memory
|
Requests: 50 mCPU, 128 MiB memory; no limits |
Configure Burstable QoS without memory limits by omitting memory limits
on both containers, because hitting a memory limit stops the node agent
and halts all resumes on the node. Raise CPU and memory requests to
match measured peak usage on a full node at your peak resume rate so
Worker Pods don't consume resources needed by the atelet
agent.
|
--upload-memory-budget flag
(objectstore-plugin container) and
--snapshot-upload-workers flag (atelet
container)
|
512 MiB budget; 2 upload workers |
To prevent concurrent snapshot uploads from queuing on multi-Actor nodes with high suspend rates, do the following:
|
--drain-timeout flag and
terminationGracePeriodSeconds field
|
5 minutes (--drain-timeout flag); 330 seconds
(terminationGracePeriodSeconds field)
|
Controls how long the atelet agent finishes in-flight
restores and snapshot uploads when its Pod stops during a node drain or
upgrade. If you increase the value of the --drain-timeout
flag for large snapshots, then also increase the value of the
terminationGracePeriodSeconds field on the
atelet DaemonSet so the kubelet agent doesn't
stop the atelet agent before uploads finish.
|
Size system infrastructure components and the database
For stateless system component Deployments in the ate-system namespace that
scale horizontally, estimate the required replica count with 35% available
capacity (the 0.65 factor) plus one spare replica (+ 1) to tolerate node
disruptions:
replicas = ceil(peak_load / (capacity_per_replica × 0.65)) + 1Horizontal scaling stops improving throughput after a shared dependency (such as PostgreSQL CPU or connection limits) reaches capacity. Size your system infrastructure components and database together. For more information about component flags, see the open source operator install guide.
The following table describes resource and scaling rules for each system infrastructure component:
| Component | QoS and resource rules | Scaling and connection tuning |
|---|---|---|
Ingress router (atenet-router) |
Configure Guaranteed QoS: set matching CPU and memory requests and limits so the Go runtime aligns its thread count with the container CPU budget. | Scale replicas horizontally based on peak ingress requests per second, mTLS overhead, and request payload sizes. |
API server (ate-api-server) |
Configure Guaranteed QoS: set matching CPU and memory requests and limits so the Go runtime aligns its thread count with the container CPU budget. |
Scale replicas based on peak resume and suspend cycles per second
(approximately 4 ms of CPU per resume and suspend cycle) and
ingress request lookup load. Explicitly set the
ateapi.postgres.poolMaxConns setting. Each replica opens up
to poolMaxConns + 6 database connections, and the sum
across all replicas must stay less than the PostgreSQL
max_connections setting.
|
| Database (PostgreSQL) |
Configure Burstable QoS with guaranteed memory: set
memory requests equal to limits to lock the shared_buffers
cache in memory and help prevent out-of-memory (OOM) eviction. Omit CPU
limits when running on a dedicated ate-postgres node so
kernel CPU throttling doesn't slow down checkpoint and autovacuum
bursts.
|
Scale this single-instance StatefulSet vertically. Provision enough CPU
to stay at less than 80% utilization at peak load, enough RAM to cache
the Actor table (with the shared_buffers parameter set to
approximately 25% of memory), and an SSD-backed StorageClass set at
installation time with the ateapi.postgres.storageClass
setting (for example, the dynamic-rwo StorageClass on C4 or
N4, or the premium-rwo StorageClass on C3 or N2). Increase
the max_connections parameter in the
postgres-config ConfigMap (the postgresql.conf
file) to fit all ate-api-server replicas.
|
Egress proxy (atenet-egress) |
Resource rules: set a CPU request of 512 mCPU (limit 8 vCPUs) and a memory request of 512 MiB (limit 1 GiB). Increase requests and limits if outbound Actor traffic experiences increased latency. | Scale replicas horizontally based on Actor outbound request traffic. |
Worker controller (ate-controller) |
Resource rules: keep at one replica (no leader election). Set CPU and memory requests based on peak usage during large WorkerPool scale-out events. |
If new Worker Pods take too long to register in large fleets, then
increase the value of the --worker-sync-concurrency flag
(default 2).
|
Certificate controller (podcertificate-controller)
|
Resource rules: runs in the
podcertificate-controller-system namespace to sign mTLS
certificates for new Worker Pods before they can accept Actors.
|
For fleets of thousands of Workers, increase the values of the
podcert.workersPerSigner setting (default 0,
which keeps the manifest default of 1 worker per signer),
the --kube-api-qps flag (default 5), and the
--kube-api-burst flag (default 10).
|
Identity provider (ate-idp-server) |
Resource rules: serves the OpenID Connect (OIDC)
discovery document and JSON Web Key Set (JWKS) in the
ate-system namespace for verifying Actor JSON Web Tokens
(JWTs). Set CPU and memory requests based on peak JWKS query volume.
|
Scale replicas horizontally if relying parties query the JWKS endpoint at high request rates without caching keys. |
Credential provider (k8s-credential-provider)
|
Resource rules: runs in the ate-system
namespace when Kubernetes Secret credential injection is enabled. Set
CPU and memory requests based on outbound credential-injection volume.
|
Scale replicas horizontally based on the rate of outbound Actor requests
that require Kubernetes Secret credential injection at the
atenet-egress proxy.
|
Isolate system infrastructure and worker node pools
In production clusters, high-churn Actor scheduling and snapshot traffic on worker nodes can compete with system infrastructure components for CPU, memory, and disk I/O. Separate your GKE cluster into three distinct node pool tiers:
- Database node pool (
ate-postgreslabel): a dedicated node pool for the PostgreSQL StatefulSet, which stores all Actor and Worker metadata. - System infrastructure node pool (
ate-system-infralabel): a dedicated node pool for the remaining system infrastructure services (theatenet-router,ate-api-server,atenet-egress,ate-controller,ate-idp-server, andk8s-credential-providercomponents). - Worker node pools (WorkerPool): dynamically scaled node pools managed by GKE custom ComputeClasses and GKE capacity buffers to host active Actor sandboxes.
You can isolate the database and system infrastructure components onto dedicated
nodes when you install Agent Substrate on GKE by using
the cordonSystemInfraComponents setting (or the
--cordon-system-infra-components flag). Before you install, you must create
both node pools with the required labels and NoSchedule taints:
# 1. Create the dedicated PostgreSQL node pool
gcloud container node-pools create ate-postgres-pool \
--cluster=CLUSTER_NAME \
--location=LOCATION \
--node-locations=POSTGRES_ZONE \
--machine-type=POSTGRES_MACHINE_TYPE \
--num-nodes=1 \
--node-labels=ate.dev/workloadType=ate-postgres \
--node-taints=ate.dev/workloadType=ate-postgres:NoSchedule
# 2. Create the dedicated system infrastructure node pool
gcloud container node-pools create ate-system-infra-pool \
--cluster=CLUSTER_NAME \
--location=LOCATION \
--machine-type=SYSTEM_INFRA_MACHINE_TYPE \
--num-nodes=2 \
--node-labels=ate.dev/workloadType=ate-system-infra \
--node-taints=ate.dev/workloadType=ate-system-infra:NoSchedule
Replace the following:
CLUSTER_NAME: the name of your GKE Standard cluster.LOCATION: the Compute Engine zone or region of your cluster's control plane.POSTGRES_ZONE: a single Compute Engine zone for the single-replica PostgreSQL StatefulSet. For a regional cluster, specify one zone within the cluster's region so that the node pool provisions a single node in the zone where the StatefulSet's persistent disk will be created. For a zonal cluster, specify the cluster's zone.POSTGRES_MACHINE_TYPE: the Compute Engine machine type sized for your PostgreSQL instance.SYSTEM_INFRA_MACHINE_TYPE: the Compute Engine machine type sized for your system infrastructure components (for example, thec3-standard-8machine type).
In a regional cluster, the --num-nodes flag applies per zone (for example,
setting the --num-nodes=2 flag across three zones creates six nodes total). To
place the ate-system-infra-pool node pool in specific zones, add the
--node-locations flag.
If you enable the cordonSystemInfraComponents setting during installation and
either node pool is missing, the corresponding system Pods remain in the
Pending state.
Automate hardware targeting using custom ComputeClasses
GKE custom ComputeClasses let you define prioritized lists of Compute Engine machine series for your WorkerPool nodes and automatically fall back to secondary machine series if a zone experiences resource unavailability.
Select Compute Engine machine series by runtime
Configure a separate ComputeClass for each sandbox runtime:
- MicroVM runtime (Cloud Hypervisor): set the
enableNestedVirtualization: truefield in your ComputeClass priorities and target x86 Compute Engine machine series that support nested virtualization (such as N4, C3, C4, or N2), or use x86 bare-metal machine types, which run KVM natively and don't require theenableNestedVirtualizationfield. Arm-based VMs and machine series listed in Nested virtualization restrictions aren't supported. - gVisor runtime: target general-purpose or compute-optimized x86 Compute Engine machine series (such as N4, C3, C4, or N2) without nested virtualization enabled. Machine series that run on mixed CPU microarchitectures (such as E2) and Arm-based VMs aren't supported for Agent Substrate on GKE. All WorkerPool resources that an ActorTemplate selects must share the same CPU architecture.
For a detailed comparison, see Select a sandbox runtime.
Required ComputeClass rules for Agent Substrate
When you create a ComputeClass for Agent Substrate on GKE worker nodes, follow these rules:
- Use node-based ComputeClasses: don't use Pod-based billing (the
podFamilyfield), which doesn't support nested virtualization. - Don't set the
nodePoolConfig.sandbox.type: microvmfield: that setting enables the Kata runtime for GKE Sandbox on Pod-based workloads and applies asandbox.gke.io/runtime=microvmtaint that prevents Agent Substrate on GKE Workers from scheduling. Agent Substrate on GKE manages its own microVM runtime and requires only Compute Engine nested virtualization on standard Container-Optimized OS (cos_containerdimage type) nodes. Configure Linux kernel
sysctlsettings for high sandbox density: if a worker node runs more than approximately 800 concurrent sandboxes, you must increase the Linux kernel limits for file system monitoring (fs.inotify.*parameters) and for the Address Resolution Protocol (ARP) neighbor table (net.ipv4.neigh.default.gc_thresh*parameters). The installer applies these settings only to clusters that it creates. When you configure custom ComputeClasses (by using thepriorityDefaults.nodeSystemConfig.linuxNodeConfig.sysctlsfield) or pre-create worker node pools (by using the--system-config-from-fileflag), apply the followingsysctlvalues yourself:fs.inotify.max_user_instances:65536fs.inotify.max_user_watches:1048576net.ipv4.neigh.default.gc_thresh1:4096net.ipv4.neigh.default.gc_thresh2:16384net.ipv4.neigh.default.gc_thresh3:32768
For more information about configuring node
sysctlparameters, see Sysctl configuration options.
Choose a scale-up pattern for microVM WorkerPool resources
MicroVM Worker Pods request the ate.dev/kvm: 1 extended resource, which is
advertised by the atelet device plugin only after a node boots and the
atelet agent starts. Because GKE node auto-provisioning doesn't
see the ate.dev/kvm extended resource on unprovisioned node templates, a
pending microVM Worker Pod can't trigger GKE to auto-create a new
node pool from zero replicas on its own. However, if a node pool already has at
least one running node, GKE can scale that node pool up.
The following table compares the supported patterns for scaling microVM WorkerPool resources on GKE:
| Pattern | How it works | Scales to 0 nodes? | Multi-priority fallback for resource unavailability? | Best used when |
|---|---|---|---|---|
Auto-created pools with the minimumCapacity field
|
Sets the nodePoolAutoCreation.enabled: true and
minimumCapacity.targetNodeCount: 1 fields with explicit
machineType priorities.
|
No (keeps 1 warm node running) |
Only at initial provisioning for the class-level
minimumCapacity field; mid-burst fallback requires setting
the minimumCapacity.targetNodeCount: 1 field on each
fallback entry in the spec.priorities[] field (keeping 1
warm node per priority)
|
You want automated node pool creation and always keep at least 1 worker node running. |
| Auto-created pools with capacity buffers |
Combines the nodePoolAutoCreation.enabled: true field with
an active or standby CapacityBuffer whose PodTemplate omits the
ate.dev/kvm extended resource.
|
Suspends to standby (for example, approximately 8% of running compute
cost on a c3-standard-4 node with a default 100 GiB
boot disk)
|
Yes (buffer placeholders trigger node auto-provisioning fallback across priorities) | You want fast burst scaling (1 to 4 seconds active, or 20 to 35 seconds to resume standby nodes and 50 to 85 seconds until new Workers are ready) with low idle cost. |
| Pre-created 0-node pools with node-template labels |
Pre-creates autoscaled 0-node pools with the
clusterautoscaler-nodetemplate-resources.ate.dev/kvm=4096
label and references them in the
spec.priorities[].nodepools field of the ComputeClass.
|
Yes (true 0 nodes, no idle compute cost) | Yes, across the pre-created node pools | You want true scale-to-zero without maintaining standby capacity buffers. |
Example: ComputeClass with the minimumCapacity field
When you use the minimumCapacity field, every entry in the spec.priorities
field must specify an explicit machineType field instead of only specifying
the machineFamily field:
apiVersion: cloud.google.com/v1
kind: ComputeClass
metadata:
name: substrate-microvm
spec:
priorityDefaults:
enableNestedVirtualization: true
nodeSystemConfig:
linuxNodeConfig:
sysctls:
fs.inotify.max_user_instances: 65536
fs.inotify.max_user_watches: 1048576
net.ipv4.neigh.default.gc_thresh1: 4096
net.ipv4.neigh.default.gc_thresh2: 16384
net.ipv4.neigh.default.gc_thresh3: 32768
priorities:
- machineType: n4-standard-4
- machineType: c3-standard-4
- machineType: n2-standard-4
nodePoolAutoCreation:
enabled: true
minimumCapacity:
targetNodeCount: 1
nodePoolConfig:
imageType: cos_containerd
whenUnsatisfiable: DoNotScaleUp
To target a WorkerPool to this ComputeClass, add the nodeSelector:
{cloud.google.com/compute-class: substrate-microvm} field to the WorkerPool Pod
template.
When a ComputeClass falls back across different x86 machine series (such as from
N4 to C3 or N2), restoring a durable snapshot at memory fidelity (the
SNAPSHOT_FIDELITY_MEMORY mode) requires a Worker whose CPU features are a
superset of the snapshot's CPU features. If no compatible Worker has capacity,
Agent Substrate downgrades the restore to root-file-system
fidelity (the SNAPSHOT_FIDELITY_ROOTFS mode), which preserves root file system
and durable volume data while restarting the Actor's processes.
Example: Scale from zero with pre-created 0-node pools
Save the high-density Linux kernel
sysctlsettings as asystem-config.yamlfile:linuxConfig: sysctl: fs.inotify.max_user_instances: '65536' fs.inotify.max_user_watches: '1048576' net.ipv4.neigh.default.gc_thresh1: '4096' net.ipv4.neigh.default.gc_thresh2: '16384' net.ipv4.neigh.default.gc_thresh3: '32768'Pre-create your primary and fallback nested-virtualization node pools at 0 nodes with the
--system-config-from-file=system-config.yamlflag and theclusterautoscaler-nodetemplate-resources.ate.dev/kvm=4096label so the GKE cluster autoscaler knows that the empty pool provides theate.dev/kvmextended resource:# Primary N4 pool (0 nodes) gcloud container node-pools create microvm-n4-pool \ --cluster=CLUSTER_NAME \ --location=LOCATION \ --machine-type=n4-standard-4 \ --enable-nested-virtualization \ --system-config-from-file=system-config.yaml \ --num-nodes=0 \ --enable-autoscaling --total-min-nodes=0 --total-max-nodes=10 \ --node-labels=cloud.google.com/compute-class=substrate-microvm-zero,clusterautoscaler-nodetemplate-resources.ate.dev/kvm=4096 \ --node-taints=cloud.google.com/compute-class=substrate-microvm-zero:NoSchedule # Fallback C3 pool (0 nodes) gcloud container node-pools create microvm-c3-pool \ --cluster=CLUSTER_NAME \ --location=LOCATION \ --machine-type=c3-standard-4 \ --enable-nested-virtualization \ --system-config-from-file=system-config.yaml \ --num-nodes=0 \ --enable-autoscaling --total-min-nodes=0 --total-max-nodes=10 \ --node-labels=cloud.google.com/compute-class=substrate-microvm-zero,clusterautoscaler-nodetemplate-resources.ate.dev/kvm=4096 \ --node-taints=cloud.google.com/compute-class=substrate-microvm-zero:NoScheduleReplace the following:
CLUSTER_NAME: the name of your GKE Standard cluster.LOCATION: the Compute Engine zone or region of your cluster. Using the--total-min-nodesand--total-max-nodesflags sets the autoscaling bounds across the entire node pool rather than per zone.
Reference the pre-created pools in priority order in your ComputeClass:
apiVersion: cloud.google.com/v1 kind: ComputeClass metadata: name: substrate-microvm-zero spec: nodePoolAutoCreation: enabled: false priorities: - nodepools: - microvm-n4-pool - nodepools: - microvm-c3-pool whenUnsatisfiable: DoNotScaleUpThe
priorityDefaultsfield doesn't apply to priorities that use thenodepoolsfield, because node settings come from the pre-created node pools. As with auto-created fallback priorities, resuming an Actor on a fallback machine series whose CPU features aren't a superset of the snapshot's CPU features downgrades the restore to theSNAPSHOT_FIDELITY_ROOTFSmode and restarts the Actor's processes.
For more information about ComputeClass configuration, see Control autoscaled node attributes with custom ComputeClasses.
Configure capacity buffers for WorkerPool burst scaling
When an Actor traffic spike exceeds the capacity of your running WorkerPool
Pods, new Worker Pods must wait for Compute Engine node provisioning,
atelet agent initialization, and sandbox asset prewarming before they can
serve Actors. GKE capacity
buffers move node
provisioning and atelet asset prewarming off the critical path:
- Active capacity buffers (
buffer.x-k8s.io/active-capacitystrategy): keep pre-provisioned nodes running and ready so new Worker Pods schedule in one to four seconds, at the full running cost of the Compute Engine VM. - Standby capacity buffers (
buffer.gke.io/standby-capacitystrategy): provision nodes and let theateletagent initialize and prewarm sandbox assets (therunscbinary or Cloud Hypervisor and Kata images) on disk during the standby initialization window before GKE suspends the Compute Engine VMs. This window defaults to five minutes (5m) and is configured by thebuffer.gke.io/standby-capacity-init-timeannotation on the CapacityBuffer. Increase the value of this annotation if asset prewarming takes longer than five minutes. When a burst occurs, GKE resumes the suspended nodes in approximately 20 to 35 seconds, and end-to-end WorkerPool readiness (scheduling Worker Pods, issuing Pod certificates, and registering capacity with theate-api-servercomponent) completes in approximately 50 to 85 seconds. While suspended, you pay only for suspended VM memory and boot disk storage. For example, suspended storage costs approximately 8% of the running compute cost on ac3-standard-4node with a default 100 GiBhyperdisk-balancedboot disk; the exact percentage varies with machine shape, boot disk size, and provisioned IOPS or throughput.
For more information about buffer strategies, see Strategies to reduce startup latency.
Limitations and rules for capacity buffers
When you configure capacity buffers for Agent Substrate on GKE, apply the following rules:
- Compute Engine machine-shape restrictions for standby buffers:
Compute Engine instance suspension isn't supported on VMs with
attached Local SSDs or on machine shapes with more than 208 GB of
memory (such as the
c3-highmem-88machine type or bare metal shapes). For large worker node shapes or nodes with Local SSDs, use active capacity buffers instead of standby buffers. - Avoid pointing the
scalableReffield at a 0-replica WorkerPool or microVM Deployment:- The GKE CapacityBuffer controller can't resolve a
custom resource (
kind: WorkerPool) when the pool is scaled to 0 replicas. - For microVM WorkerPool resources, also don't point the
scalableReffield at the underlying Deployment, because the Deployment Pod template includes theate.dev/kvm: 1extended resource (which prevents node auto-provisioning from scaling out an empty pool). Instead, configure buffers by referencing a PodTemplate (in thepodTemplateReffield) that selects the ComputeClass without requesting theate.dev/kvmextended resource. - For gVisor WorkerPool resources (which don't request the
ate.dev/kvmextended resource), you can point thescalableReffield directly to the WorkerPool underlying Deployment or use a PodTemplate.
- The GKE CapacityBuffer controller can't resolve a
custom resource (
Example: Standby CapacityBuffer using a PodTemplate
The following manifest defines a PodTemplate and a standby CapacityBuffer that work for either microVM or gVisor WorkerPool resources:
apiVersion: v1
kind: PodTemplate
metadata:
name: substrate-worker-buffer-template
namespace: WORKERPOOL_NAMESPACE
template:
spec:
nodeSelector:
cloud.google.com/compute-class: COMPUTE_CLASS_NAME
containers:
- name: buffer-placeholder
image: registry.k8s.io/pause:3.9
resources:
requests:
cpu: "WORKER_CPU_REQUEST"
memory: "WORKER_MEMORY_REQUEST"
---
apiVersion: autoscaling.x-k8s.io/v1beta1
kind: CapacityBuffer
metadata:
name: substrate-standby-buffer
namespace: WORKERPOOL_NAMESPACE
spec:
podTemplateRef:
name: substrate-worker-buffer-template
replicas: BUFFER_REPLICAS
provisioningStrategy: buffer.gke.io/standby-capacity
Replace the following:
WORKERPOOL_NAMESPACE: the Kubernetes namespace of your WorkerPool (for example,my-workerpool).COMPUTE_CLASS_NAME: the name of the custom ComputeClass targeted by your WorkerPool (for example,substrate-microvm).WORKER_CPU_REQUESTandWORKER_MEMORY_REQUEST: the CPU and memory requests of a single Worker Pod in your WorkerPool (for example,2CPUs and4Giof memory on a 4-vCPU node), leaving node capacity for theateletagent and GKE system daemons. Matching these values to one Worker Pod's requests helps ensure that each buffer replica provisions one standby worker node rather than packing multiple buffer placeholders onto a single node.BUFFER_REPLICAS: the number of standby worker nodes to keep prewarmed (for example,4nodes).
When you use standby capacity buffers, add a toleration for the
buffer.gke.io/standby-capacity-node-suspended taint (with the operator:
"Exists" field) to the atelet DaemonSet so that replacement atelet Pods can
remain scheduled on suspended standby nodes (for example, during a DaemonSet
rolling update). For an example DaemonSet configuration, see Preload images on
standby
buffers.
If multiple workloads share the same ComputeClass, add matching
workload-separation tolerations and nodeSelector labels to both your
WorkerPool Pod template and the buffer PodTemplate so that GKE
provisions dedicated tainted nodes that other workloads don't claim. Also add a
matching toleration to the atelet DaemonSet so that the atelet agent can
schedule on those dedicated nodes. For step-by-step instructions, see Configure
capacity buffers.
Manage upgrades and autoscaler interactions
This section describes how to manage atelet DaemonSet updates, cluster
autoscaler scale-down events, and node upgrades without disrupting active
Actors.
Update the atelet DaemonSet with standby buffers
Suspended standby buffer nodes carry the
buffer.gke.io/standby-capacity-node-suspended taint and an unreachable taint
while suspended, so the kubelet agent on a suspended node can't acknowledge
Pod deletions. Ensure that the atelet DaemonSet tolerates the
buffer.gke.io/standby-capacity-node-suspended taint (with the operator:
"Exists" field). Even with that toleration, during a standard rolling update of
the atelet DaemonSet (with the maxUnavailable: 1 field), Pods on suspended
nodes enter the Terminating state and can block the update from progressing on
running nodes.
Before you update the atelet DaemonSet on a cluster with suspended standby
nodes, do one of the following:
- So unreachable standby nodes don't exhaust the update budget, set the
spec.updateStrategy.rollingUpdate.maxUnavailablefield on theateletDaemonSet to a value greater than the maximum number of suspended standby nodes. Before you update the
ateletDaemonSet, temporarily scale your standby CapacityBuffer replicas to0and wait until the cluster autoscaler removes all suspended standby buffer nodes (typically 10 to 15 minutes). To check whether any nodes still report aSuspendedcondition status ofTrue, run the following command:kubectl get nodes -o custom-columns='NAME:.metadata.name,SUSPENDED:.status.conditions[?(@.type=="Suspended")].status'
Protect active Actors during scale-down and upgrades
Because active Actors execute inside sandboxes managed by the atelet agent
rather than as individual Kubernetes Pods, a node's CPU and memory requests
might appear low to the GKE cluster autoscaler even while a
Worker Pod on that node is hosting active Actors. When a node is drained, the
kubelet agent sends a SIGTERM signal to the Worker Pod, and the Worker
forwards the SIGTERM signal to the main process of each container of every
Actor that it hosts. Each active Actor then has up to 30 minutes to be suspended
before the Worker stops. Give every stateful Actor a SIGTERM handler that
doesn't exit on the signal and that calls the SuspendActor method with the
DURABILITY_MODE_SYNCHRONOUS mode. An Actor whose main process exits on the
SIGTERM signal loses unsaved state immediately. If the Worker Pod stops while
still hosting an active Actor, then the Actor enters the ACTOR_STATE_CRASHED
state.
To minimize disruptions during scale-down events and GKE cluster upgrades, apply the following practices:
- Avoid using Spot VMs for stateful WorkerPool nodes, because Spot VM preemption provides only 15 seconds to 2 minutes of termination notice before stopping the node.
- Coordinate draining in your orchestrator so active Actors are checkpointed
and suspended before Worker Pods stop, or prevent the cluster autoscaler
from evicting active Worker Pods (for example, by using the
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"Pod annotation). If you set thesafe-to-evict: "false"annotation statically in the WorkerPool Pod template, then the cluster autoscaler won't scale down a node while a Worker Pod is scheduled on it—even when that Worker hosts zero active Actors. To release idle nodes, you must scale down WorkerPool replicas or manage the annotation dynamically. For more information about the Worker eviction lifecycle and recovering crashed Actors with theRevertActormethod, see Eviction in the Agent Substrate API guide. - Configure GKE surge upgrades and review Upgrade in a resource-constrained environment so new worker nodes are provisioned before existing worker nodes are cordoned and drained.
What's next
- Learn how to choose the right orchestration model, sandbox runtime, isolation controls, and storage options in Plan an architecture for AI agent workloads on GKE.
- Learn more about GKE custom ComputeClasses and GKE capacity buffers.
- Explore more resources in the Agent Substrate GitHub repository.