Benchmark data and sizing rationale for running agent-sandbox-controller
under high claim rates.
Configuration is the canonical flag reference — every flag named here is defined there, and its High-Throughput & Scale Tuning section has a quick-start Deployment manifest. This guide adds what the reference deliberately leaves out: the benchmark evidence behind those recommendations, how to size each knob from your own traffic profile, and where each knob helps, is neutral, or can hurt.
All flags are on the agent-sandbox-controller binary. Defaults are safe for
small deployments; everything below is opt-in. Flags not covered here
(readiness grace period, unschedulable recheck interval, warm pool eviction,
pprof) are in the flag reference.
Most latency numbers below concern warm adoption — a SandboxClaim
binding a pre-created sandbox from a SandboxWarmPool. Two claim fields
disable warm adoption entirely: a claim that sets spec.env or
spec.volumeClaimTemplates always cold-starts a fresh sandbox from the
pool's template, even when the pool has ready capacity, because those
customizations cannot be applied to an already-running warm pod.
(spec.additionalPodMetadata does not have this effect — it is applied to
the adopted sandbox in place.) If your claims set either field, pool sizing
and refill shaping will not improve their latency.
When planning capacity or comparing benchmark numbers, distinguish between two operating regimes:
| Regime | What is on the critical path | Typical throughput ceiling |
|---|---|---|
| Burst warm adoption (draining an already-Ready warm pool) | Optimistic-lock Sandbox claim binding + SandboxClaim status update (no Pod creation or scheduling during the burst) |
~300 claims/s per cluster at p90 ≤ 200 ms (e.g., 3,600 claims over a 12 s burst into a pre-warmed 3,700-replica pool) |
| Sustained warm adoption + concurrent refill (continuous claims where the pool must refill at the arrival rate) | Warm adoption plus Sandbox + Pod creation, warmPoolExpectations watch round-trips, kube-scheduler pod binding, and node container startup |
~70–85 sandboxes/s per SandboxWarmPool (bounded by per-pool workqueue serialization, expectations-gate round-trips, and kube-scheduler's default --kube-api-qps=50) |
- Per-pool workqueue serialization:
SandboxWarmPoolReconcilerserializes reconciles per pool key. Raising--sandbox-warm-pool-concurrent-workersonly parallelizes across distinctSandboxWarmPoolresources, not within a single pool. - Per-pool expectations gate: After issuing a batch of
Sandboxcreates, a pool'swarmPoolExpectationstracker blocks further creates for that pool until the controller's informer cache observes the correspondingADDwatch events (or the 5-minute fallback timeout expires). If watch delivery lags for 10–30 s under heavy write load, a single pool freezes all replenishment for its entire replica count. - Unpaced refill bursts: With the default
--sandbox-warm-pool-max-refill-rate=0, a large deficit triggers a batch of up to--sandbox-warm-pool-max-batch-size(default300) parallelSandboxcreates at once. Because eachSandboxobject is ~10 KB in etcd (inlinedpodTemplate+managedFields), firing hundreds of creates in a single reconcile spikes etcd MVCC growth, saturates the API server watch ring buffer, and competes with latency-critical claim adoptions.
For sustained claim rates above ~30 claims/s or warm pools larger than ~500–1,000 replicas:
-
Split capacity across multiple
SandboxWarmPoolresources (for example, 4 pools of 450 replicas instead of 1 pool of 1,800 replicas) and set--sandbox-warm-pool-concurrent-workersto match the number of active pools. -
Set
--sandbox-warm-pool-max-refill-rate(e.g.,50–100per pool when watch delivery is healthy) so replenishment flows as a steady token-bucket stream rather than 300-sandbox bursts.-
Caveat under watch lag: The expectations gate admits one create batch
per watch round-trip, and
refillBucketcaps each batch atmax(1, rate)tokens (1 second of creates). Under an$L$ -second watch lag, effective per-pool refill drops to$\approx \min(\text{rate}, \text{batchSize}) / L$ (for example,rate=100with a 15 s lag yields only ~7 creates/s per pool, below the unpaced300 / 15 = 20/s). When watch lag is present, the primary levers are pool count and--sandbox-warm-pool-max-batch-size, and the per-pool rate must be sized as$\text{rate} \ge \text{claims/s} \times L / \text{pools}$ rather than a flat50–100.
-
Caveat under watch lag: The expectations gate admits one create batch
per watch round-trip, and
-
Isolate watch streams with
--separate-watch-connection=trueand--api-connections=4so mutating write traffic cannot starve the informer watch events that clear each pool's expectations gate.
These numbers come from two sources:
PR-level benchmarks (kops, k8s 1.35.6, e2-standard-16 control plane): the authoritative warm-adoption numbers, run against a dedicated cluster under controlled conditions.
GKE live tests (k8s 1.36.2, 20× e2-standard-16 worker nodes, 75-claim burst from a 150-sandbox warm pool): two rounds of A/B comparison run against the same live cluster. Pod cold-start was ~42–50 s p50 on this cluster; warm-adoption latency (sub-second when the pool is healthy) is covered by the PR-level data. The relative impact of each flag combination is the signal.
Worker count sweep, all other perf flags at recommended values (GKE, 75-claim
burst from a 150-sandbox warm pool). Worker quadruples are
sandbox/claim/warm-pool/template:
| Config | p50 | p90 | p99 | n/75 |
|---|---|---|---|---|
| Baseline — no perf flags, 1000/1000/500/100 workers | 47.3 s | 85.3 s | 93.8 s | 75/75 |
| Recommended workers (200/150/2/1) + all perf flags | 49.8 s | 89.5 s | 98.2 s | 75/75 |
| Minimal workers (50/50/1/1) + all perf flags | 55.6 s | 95.6 s | 104.5 s | 75/75 |
| High workers (1000/1000/500/100) + all perf flags | 52.7 s | 93.5 s | 102.3 s | 75/75 |
--cache-label-selectors only, baseline workers |
49.5 s | 89.8 s | 98.5 s | 75/75 |
What this shows:
- Worker counts 200/150/2/1 are validated. The recommended values beat both high workers and minimal workers at this scale. High worker counts add unnecessary API server contention; too-few workers create a reconcile bottleneck.
- 50/50/1/1 has a measurable regression — p50 +11%, p90 +7% vs recommended values. There is a real floor below which under-provisioning hurts.
--cache-label-selectorsis neutral on a small cluster. On clusters with5000 total pods it reduces informer scope from O(cluster pods) to O(sandbox pods) — the benefit scales with how many non-sandbox pods the cache would otherwise hold.
- Perf flags add ~5% cold-start overhead vs a no-flags baseline. This is expected: the flags reduce write contention during warm-adoption bursts, not cold pod scheduling latency.
| Config | p50 | p90 | p99 | n/75 | Notes |
|---|---|---|---|---|---|
| A: Baseline (workers 1000/1000/500/100, batch 500) | 50.4 s | 88.2 s | 96.8 s | 75/75 | cold-start floor |
B: + --api-connections=4 --separate-watch-connection |
51.2 s | 92.9 s | 102.0 s | 75/75 | neutral |
C: + --disable-claim-events --disable-claim-observability-annotations |
51.1 s | 93.0 s | 101.7 s | 75/75 | neutral |
D: + --sandbox-warm-pool-replenish-delay=15s |
41.5 s | 74.6 s | 82.2 s | 75/75 | neutral on its own |
E: + --sandbox-warm-pool-max-refill-rate=80 |
42.2 s | 75.3 s | 82.7 s | 75/75 | −15% vs baseline |
G: + --sandbox-warm-pool-max-refill-rate=100 |
41.6 s | 74.6 s | 82.0 s | 75/75 | same as rate=80 |
| H: sustained profile below (all flags + rate=100, no delay) | 41.4 s | 74.2 s | 82.6 s | 75/75 | ✅ confirmed optimal |
| I: burst profile below (all flags + delay=20s + rate=100) | 49.4 s | 204.7 s | 214.5 s | 74/75 |
What this shows:
--sandbox-warm-pool-max-refill-ratedelivers a consistent ~15% reduction in p50/p90 by smoothing the pod-creation burst and reducing scheduler contention. Rate=80 and rate=100 perform identically, so the exact value matters less than enabling it.--api-connections,--separate-watch-connection, and write-reduction flags are neutral on per-claim latency at this scale (75 concurrent claims). Their value appears at higher scale where the API server concurrency ceiling is hit.--sandbox-warm-pool-replenish-delayis neutral in isolation (config D). However, when combined with other flags (config I), a single transient API timeout during claim creation (1/75 failed) allowed the pool to drain — because the delay prevented immediate refill — producing a catastrophic p90 of 205 s. Do not enable this flag unless you can guarantee zero claim creation failures under load.- Config H (the sustained profile below) is confirmed optimal across all 75/75 claims with no failures.
Under a 45/s Poisson arrival rate across 4 pools (113 replicas/ns) on kops:
| Config | p50 | p90 | p99 |
|---|---|---|---|
Burst-tuned (replenish-delay=20s, no rate cap) |
~57 ms first ~10 s, then ~1.4 s cold | 2.28 s steady-state | — |
Sustained-tuned (replenish-delay=0, max-refill-rate=100) |
92 ms | 182 ms | 376 ms |
The sustained config held p50 flat at 77–116 ms across all six 10-second
windows with no pool drain. Per-pool refill ceiling is ~70–85 sandboxes/s
(pod scheduling bounds at ~70/s at the kube-scheduler default
--kube-api-qps=50).
Two-configuration A/B (300-claim warm burst + 45/s × 60s sustained Poisson, 12-node kops cluster, e2-standard-16 control plane):
| Config | burst p50/p90 | sustained p50/p90 | pod PATCHes ok | optimistic 409s |
|---|---|---|---|---|
Baseline (window=0) |
1465 / 3201 ms | 320 / 680 ms | 2,953 | 5,447 |
--sandbox-write-behind-window=250ms |
1271 / 3158 ms | 507 / 1027 ms | 2,997 | 3,077 |
Key observations:
- 44% fewer 409 conflicts (5,447 → 3,077) — the most reliable benefit at scale.
- Burst p50 −13% (1465 → 1271 ms) — write coalescing frees API seats for claim adoption.
- Sustained p50 +58% (320 → 507 ms, p90 680 → 1027 ms) — a significant regression under continuous churn; see the write-behind section for when the trade-off is worth it.
- +10% sandbox reconcile count (one extra deferred pass per coalesced write) at ~+2.5% total writes.
- Pod patch bound is always ≤
min(window, 1s)— thesafe-to-evictstrip cannot lag the cluster autoscaler.
Flag reference: API Transport and Connection Settings.
The kube-apiserver caps concurrent in-flight HTTP/2 streams per connection
(100 by default, configurable server-side via
--http2-max-streams-per-connection), so a single connection bounds the
controller's effective request concurrency regardless of worker count.
--api-connections=N shards non-watch traffic (writes, uncached reads,
events, leader election) round-robin across N dedicated connections;
--separate-watch-connection isolates the informer watch streams from
write-burst congestion. Enable them together.
When they matter: live testing showed both are neutral on per-claim latency at 75 concurrent claims. The benefit appears when concurrent in-flight requests approach the per-connection stream limit (~100) — i.e. large worker counts with sustained bursts — where a shared connection also starves watch delivery and can wedge warm-pool refill on the expectations gate (see High-Throughput & Scale Tuning).
--kube-api-qps / --kube-api-burst: the default QPS of -1 means no
client-side throttle — the server's APF policy controls admission instead,
which is the preferred approach (see APF insulation
below). If you must apply client-side throttling, set --kube-api-burst ≥
the total worker count (sandbox + claim + warm-pool + template); the
controller logs a startup warning when it detects a positive QPS with a burst
below the worker total.
The most important server-side tuning is applying the APF insulation overlay
at examples/apf-insulation/apf-insulation.yaml. This gives the controller
dedicated APF concurrency seats and splits its traffic into three priority
classes so refill bursts cannot queue out claim-adoption writes.
See docs/apf-insulation.md for the full explanation, seat-sizing math, and when to apply it.
Recommended: apply on any cluster where claim rates exceed ~50/s or where other workloads share the API server.
Workers control how many reconciles run in parallel within each controller. Raising workers increases throughput but also increases API server load.
| Flag | When to raise | Practical ceiling |
|---|---|---|
--sandbox-concurrent-workers |
Many sandboxes in flux simultaneously | ~500; bounded by node scheduling capacity |
--sandbox-claim-concurrent-workers |
High claim arrival rate | ~200; bounded by API server write throughput |
--sandbox-warm-pool-concurrent-workers |
Multiple warm pools | Size to the number of active pools — reconciles for one pool are serialized, so extra workers only help across distinct pools |
--sandbox-template-concurrent-workers |
Many templates changing frequently | rarely > 2 |
Worker counts 200/150/2/1 are directly benchmarked — they outperform both
high workers (1000/1000/500/100) and minimal workers (50/50/1/1) at 75
concurrent claims (see the worker A/B table above). The benchmark ran a
single pool; per the sizing rule, --sandbox-warm-pool-concurrent-workers=1
is equivalent for one pool.
Warning: sandbox + claim + warm-pool + template workers totaling > 1000
triggers a startup warning. The practical ceiling is usually the API server's
mutating inflight limit, not the worker count.
When a burst of SandboxClaims adopts warm sandboxes, the warm-pool controller immediately tries to refill the deficit. Without shaping, this refill burst competes for API server write capacity with the very claim-adoption writes it is meant to support. Flag reference: Warm Pool Replenishment Shaping.
Maximum sandboxes created per reconcile round. Creates advance one observed
batch per watch round-trip (the expectations gate waits for a batch's ADD
events before issuing the next), so a refill deficit of D sandboxes clears
in about ceil(D / batchSize) round-trips — an initial pool fill is the
D = replicas case. Raising it trades round-trips for burst size and is safe
at any value under the gate. When a refill rate cap is set (below), the token
bucket usually bounds each batch before the batch cap does.
Caps the refill rate in sandboxes/second per pool via a per-pool token bucket. Turns full-deficit burst creates into a smooth stream, reducing scheduler contention. When no token is available the controller requeues for exactly when the next token accrues; partial batches advance on the watch events of their own creates. The cap paces all pool creates, including initial fill and scale-ups — live verification of a 150-replica pool at rate=100 filled in two paced batches (100, then 50 a second later).
Live testing confirmed a consistent ~15% reduction in p50 and p90 latency (baseline 50.4 s → 41.4–42.2 s p50; 88.2 s → 74.2–75.3 s p90). Rate=80 and rate=100 produced statistically identical results, so the exact value above ~50/s is not critical.
This flag is safe to enable broadly — it helps even when claims fall through to cold start, and it has no failure mode.
Sizing: set per-pool rate ≥ your steady per-pool claim arrival rate to prevent pool drain. If you have 4 pools and 80 claims/s total, 20/s per pool is sufficient; 100/s leaves headroom for spikes. The per-pool ceiling on a standard control plane is ~70–85/s. (The quick-start manifest uses rate=25, sized for a ~20 claims/s workload by the same rule.)
Defers the start of refill after pool members are adopted. While claims are still arriving, the timer re-arms; it only fires once the pool is stable for the delay duration. Initial pool fill and scale-ups are never delayed by this flag (though the rate cap above, if set, still paces them).
In isolation, this flag is neutral — live testing of replenish-delay=15s
alone produced the same latency as max-refill-rate=100 alone (p50=41.5 s,
p90=74.6 s). The flag's benefit only appears when warm adoption is actually
occurring and the pool is large enough to absorb the full burst.
replenish-delay=0.
When to use: only enable this flag if warm adoption is confirmed (claim p50 in the sub-second range) and your pool is consistently larger than your maximum burst size with headroom for transient failures. Monitor claim p90 continuously after enabling — any jump is the signature of pool drain.
The two flags compose: replenish-delay defers the start of refill;
max-refill-rate shapes its flow once started. In most cases max-refill-rate
alone gives the same performance with none of the fragility.
Scopes the Pod and Service informer caches to objects carrying the sandbox
tracking label (agents.x-k8s.io/sandbox-name-hash). The controller only
ever creates Pods and Services it labeled itself, so on large or shared
clusters this cuts informer list/watch volume, JSON decode CPU, and cache
memory from O(cluster pods) to O(sandbox pods).
Recommended: enable on clusters with > ~5000 total pods or where the controller's pod cache is a meaningful fraction of memory. Benchmarked in isolation on a small clean cluster (worker A/B table above): neutral at small scale (p50 49.5 s vs 47.3 s baseline) as expected — the benefit scales with how many non-sandbox pods the cache would otherwise track.
Caveat: externally pre-provisioned resources that rely on the
agents.x-k8s.io/adoptable=true adoption path must also carry the tracking
label (value = the owning sandbox's name hash) to remain visible to the
controller when this flag is enabled.
During a claim burst, every avoidable write competes for API server capacity with latency-critical adoption writes. Flag reference: API Write and Cache Optimization.
Disable Kubernetes Event emission from the Sandbox and SandboxClaim
controllers respectively. Events (SandboxPodCreated, SandboxReady,
SandboxAdopted, etc.) are informational only; removing them eliminates
roughly 1–2 API writes per sandbox and claim lifecycle transition.
Live testing showed no measurable per-claim latency improvement in isolation at 75 concurrent claims; the benefit appears during large warm-pool fills, when the events API level in APF is saturated, or at claim rates above ~200/s.
Skips the dedicated API write that persists the SandboxClaim observability annotations (controller first-observed timestamp, trace context). The values are still stamped on the in-memory object, so startup-latency metrics and trace propagation within the current process keep working.
Treat this flag as removing one write per claim, not as a guarantee the annotations never appear on the object: when a later full-object update carries the claim (e.g. the adoption update on the warm path), the stamped values are persisted along with it. The write saving holds either way.
Coalesces the Sandbox controller's recoverable pod metadata patch via
RequeueAfter deferral. The specific write deferred is the pod
label/annotation reconciliation on the warm-pool adoption path (warm-pool
label prune, cluster-autoscaler.kubernetes.io/safe-to-evict strip,
propagated-keys tracking). When the window is open, the reconcile pass skips
the patch and returns RequeueAfter: <remaining window>; the workqueue's
per-key dedup coalesces repeated redeliveries into a single flush pass that
recomputes the desired state from fresh informer state and issues one merge
patch.
Properties:
- Sandbox readiness is never gated on the deferred write — the claim Ready condition is set from in-memory state without waiting for the flush.
- Pod patch is always flushed within
min(window, 1s), bounding thesafe-to-evictannotation lag well below the cluster autoscaler's 10 s scan interval. - Crash-safe: the deferral clock stores timestamps only, no mutation payload. A crash merely restarts a sub-second window on the next leader's first pass.
Trade-offs (benchmarked at 250ms):
| Baseline (0) | 250ms | |
|---|---|---|
| 409 write conflicts | 5,447 | 3,077 (−44%) |
| Burst p50/p90 | 1465/3201 ms | 1271/3158 ms (−13% p50) |
| Sustained p50/p90 | 320/680 ms | 507/1027 ms (+58% p50) |
| Sandbox reconciles | +0% | +10% |
The sustained regression is significant: +58% p50 (320 → 507 ms) and +51% p90 (680 → 1027 ms) under continuous churn.
Recommended: enable --sandbox-write-behind-window=250ms only when 409
write conflicts are high (watch for them in controller logs) or the workload
is burst-dominated, and your latency SLO tolerates ~500 ms sustained
warm-adoption p50. Leave it at 0 when sustained warm-adoption latency is the
primary constraint — which is why the sustained profile below omits it.
| Symptom | Root cause | Fix |
|---|---|---|
Warm pool stops refilling for 10 s–5 m while claims drain the pool (warmPoolExpectations gate stays closed) |
Watch ADD events for newly created Sandbox objects are queued behind mutating write bursts on a shared HTTP/2 connection, or kube-apiserver is slow decoding ~10 KB Sandbox watch payloads |
Enable --separate-watch-connection=true, --api-connections=4, and pace/shard across multiple SandboxWarmPools; ensure ConcurrentWatchObjectDecode (KEP-6178; beta and default-off in Kubernetes 1.31–1.36, default-on in 1.37+) is enabled on the API server |
Sandbox watch force-closed (410 Gone / relist) or etcd DB grows to multiple GBs between defrags |
Unpaced 300-sandbox refill bursts writing ~10 KB per Sandbox (inlined podTemplate + managedFields) plus Pod specs saturate kube-apiserver watch delivery and bloat etcd MVCC revisions |
Pace refills with --sandbox-warm-pool-max-refill-rate; enable --disable-sandbox-events, --disable-claim-events, and --disable-claim-observability-annotations; tune the managed control plane (below) |
High claim adoption latency while warm sandboxes are Ready (workqueue wait or APF queuing) |
Warm-pool refill POST requests share workload-low in APF and starve latency-critical claim adoption PATCHes |
Apply examples/apf-insulation/apf-insulation.yaml (APF Insulation) and pace refills with --sandbox-warm-pool-max-refill-rate |
High rate of HTTP 409 Conflict errors in controller logs during bursts |
Excessive worker concurrency colliding on Sandbox or Pod resourceVersion updates |
Keep workers near the validated 200/150 profile (avoid 1000+ workers); optionally set --sandbox-write-behind-window=250ms for burst-dominated workloads |
Each Sandbox custom resource stores an inlined PodTemplate plus
metadata.managedFields (~10 KB per object in etcd). Although the controller
strips managedFields and Pod.spec in its local informer cache, every
Sandbox create, adoption, or status write still serializes the full object
through etcd and the kube-apiserver watch cache. At 30+ sustained claims/s
(~100+ Sandbox writes/s alongside Pod writes), the Kubernetes control plane
itself can become the bottleneck.
When running on a managed control plane where you cannot edit kube-apiserver
or etcd flags directly, check or request the following from your cloud
provider:
- Dedicated / higher control-plane tier & etcd disk IOPS: Rapid multi-GB etcd database growth between defragmentations or multi-second stalls across all API server replicas during etcd defrag indicates saturated etcd disk throughput/IOPS or synchronized defragmentation. Upgrade to a dedicated/high-scale control-plane tier and verify that etcd defragmentation runs rolling (one member at a time behind leader transfer).
ConcurrentWatchObjectDecode(KEP-6178):ConcurrentWatchObjectDecodeletskube-apiserverdecode ~10 KBSandboxwatch payloads concurrently instead of falling 10–15 s behind on the controller's watch stream. It is beta but default-off in Kubernetes 1.31–1.36 and becomes default-on in 1.37+ — on 1.31–1.36 managed control planes, explicitly enable it (or ask your provider to enable the feature gate).- API server inflight seats &
kube-schedulerQPS: Default Kubernetes API server limits (--max-requests-inflight=400,--max-mutating-requests-inflight=200) leave only ~77 concurrency seats foragent-sandbox-criticalunder the APF overlay. Scaling API server inflight limits (e.g., 1500–3000 read / 500–1000 mutating) and raisingkube-scheduler--kube-api-qpsabove its default50removes the control-plane concurrency and pod-scheduling ceiling (see docs/apf-insulation.md).
These are the exact configurations validated in the GKE live A/B above. The quick-start manifest in configuration.md is a fine starting point for moderate sustained workloads (~20 claims/s, single pool); the profiles below show how the same sizing rules apply at benchmark scale, and what to change for burst-dominated traffic.
Traffic pattern: continuous high arrival rate (e.g. 30–100 claims/s steady). Priority: keep the pool topped up at the arrival rate; warm latency must hold across the entire window.
This is config H from the live benchmark — 75/75 claims, p50=41.4 s, p90=74.2 s, no failures across all runs. It is the default recommendation for any high-throughput deployment.
args:
- --extensions
# API server connections (benefit when in-flight requests approach the
# ~100-stream per-connection limit)
- --api-connections=4
- --separate-watch-connection=true
# Concurrency — validated at 75 concurrent claims; outperforms 1000/1000/500/100.
# Size warm-pool workers to the number of active pools (benchmark: single pool).
- --sandbox-concurrent-workers=200
- --sandbox-claim-concurrent-workers=150
- --sandbox-warm-pool-concurrent-workers=2
# Warm pool: smooth refill, no delay (size rate ≥ per-pool claim arrival rate)
- --sandbox-warm-pool-replenish-delay=0
- --sandbox-warm-pool-max-refill-rate=100
# Cache (benefit on clusters with >5000 total pods)
- --cache-label-selectors=true
# Write reduction (benefit during large pool fills or at ≥200 claims/s)
- --disable-sandbox-events=true
- --disable-claim-events=true
- --disable-claim-observability-annotations=true
# NOT set: --sandbox-write-behind-window — costs +58% sustained p50; enable
# only for burst-dominated traffic or high 409 rates (see Write reduction)Also apply examples/apf-insulation/apf-insulation.yaml to the cluster.
Sizing --sandbox-warm-pool-max-refill-rate: set it ≥ your per-pool
steady claim arrival rate. Rate=80 and rate=100 were statistically identical
in testing; setting 100 provides headroom for spikes. The per-pool ceiling is
~70–85/s on a standard control plane.
Traffic pattern: many claims arrive together, then the cluster is quiet. Priority: serve the burst at warm latency; refill can wait.
Prerequisites before enabling replenish-delay:
- Confirm warm adoption is working (claim p50 is sub-second, not tens of seconds).
- Confirm your pool is consistently larger than your maximum burst size, with headroom for transient claim creation failures under API pressure.
- Have monitoring on claim p90 in place — any jump is pool drain.
If you cannot confirm all three, use the sustained profile above instead. It delivers equivalent performance without the fragility.
args:
- --extensions
- --api-connections=4
- --separate-watch-connection=true
- --sandbox-concurrent-workers=200
- --sandbox-claim-concurrent-workers=150
- --sandbox-warm-pool-concurrent-workers=2
- --sandbox-warm-pool-replenish-delay=20s # ⚠️ see prerequisites above
- --sandbox-warm-pool-max-refill-rate=100
- --cache-label-selectors=true
- --disable-sandbox-events=true
- --disable-claim-events=true
- --disable-claim-observability-annotations=true
- --sandbox-write-behind-window=250ms # burst-dominated traffic: −13% burst p50, −44% 409sAlso apply examples/apf-insulation/apf-insulation.yaml to the cluster.
For a complete tuned Deployment manifest (selector, service account, image),
see the
High-Throughput Deployment Example
in the flag reference. The warm-pool and claim flags only take effect on a
deployment running with --extensions.
Live cluster patch: use a strategic merge patch that targets the
container by name and replaces the whole args list. Unlike appending
individual args with a JSON patch, this is idempotent (re-running it cannot
duplicate flags) and does not assume the controller is at container index 0:
kubectl patch deployment agent-sandbox-controller \
-n agent-sandbox-system \
--type=strategic \
-p '{"spec":{"template":{"spec":{"containers":[{
"name":"agent-sandbox-controller",
"args":[
"--leader-elect=true",
"--extensions",
"--api-connections=4",
"--separate-watch-connection=true",
"--sandbox-concurrent-workers=200",
"--sandbox-claim-concurrent-workers=150",
"--sandbox-warm-pool-concurrent-workers=2",
"--sandbox-warm-pool-max-refill-rate=100",
"--cache-label-selectors=true",
"--disable-sandbox-events=true",
"--disable-claim-events=true",
"--disable-claim-observability-annotations=true"
]}]}}}}'Because the patch replaces the full list, include every flag the deployment
needs (including --leader-elect and --extensions shown above), not just
the tuning flags. Check the current list first with:
kubectl get deployment agent-sandbox-controller -n agent-sandbox-system \
-o jsonpath='{.spec.template.spec.containers[?(@.name=="agent-sandbox-controller")].args}'