Appendix A — Capacity Targets & SLOs¶
← Glossary | Back to index | Next: Appendix B — Worker Isolation →
The following targets are conservative defaults derived from the architectural constraints in §2 and §3.5. They are intended as starting points to be refined against real production data; operators are expected to override them based on their cluster size, GitHub plan, and workload profile.
Latency SLOs (per-job, per-tenant)¶
| Metric | Target | Source | Note |
|---|---|---|---|
| Pod-creation latency (p95) | ≤ 15s | actions_gateway_pod_creation_latency_seconds |
From worker pod creation to runner container start (scheduling + image pull). Dominated by image pull on cold nodes; sub-second on warm. |
| Pod-creation latency (p99) | ≤ 60s | actions_gateway_pod_creation_latency_seconds |
Tolerates cold-start image pull. |
| Session reacquisition after Actions Gateway Controller (AGC) restart | ≤ 2 min | derived | Equal to GitHub's redelivery window; jobs redelivered within this window suffer no observable disruption. |
| Token refresh failure budget | < 1 / hour | actions_gateway_token_refresh_errors_total |
Anything above this rate indicates either GitHub API instability or a credential problem. |
Capacity Targets (per-AGC pod, single tenant)¶
| Resource | Target | Rationale |
|---|---|---|
| Concurrent virtual sessions (peak burst) | ≤ 1,000 | Memory-bound burst ceiling: each goroutine stack + HTTP buffer + token-manager indirection averages ~60 KiB resident (a deliberately conservative sizing figure — the AGC's own per-session structures measure ~12 KiB, see Per-session memory & density); 1,000 sessions ≈ 60 MiB at peak. Steady-state cost is 1 session per RunnerGroup, far below this ceiling for typical deployments. |
| Memory request | 2 GiB | Sized for the peak burst ceiling of 1,000 concurrent goroutines (~60 MiB) with 4× safety margin for Go runtime overhead, heap churn, and reconcile storms. Actual steady-state resident size will be much smaller. |
| Memory limit | 4 GiB | Allows transient bursts during reconcile storms without triggering OOM. |
| CPU request | 500m | Predominantly I/O-bound; request reflects baseline scheduling weight rather than steady CPU draw. |
| CPU limit | 2 (cores) | Permits short bursts during reconcile churn or token refresh contention without throttling. |
Capacity Targets (per GitHub App installation)¶
| Resource | Target | Source |
|---|---|---|
| Concurrent sessions per installation | ≤ 250 | Bounded by §3.5 rate-limit math: ~72 message polls/hr/session against the 15,000/hr installation budget. |
Sustained RateLimited condition |
< 1 min | Anything longer indicates the operator is over budget and should shard across installations. |
Capacity Targets (per proxy pod)¶
| Resource | Target | Note |
|---|---|---|
| Concurrent CONNECT tunnels | ≤ 500 | File-descriptor-bound; tune the proxy pod ulimit nofile if increasing. |
| CPU request / limit | 10m / 500m | Defaults per ProxyConfig. The 500m limit (not 100m) keeps the pod from throttling before the HPA's 60%-utilization signal trips under CONNECT load. Adjust upward if HPA lag is observed under bursty load. |
| Memory request / limit | 32 MiB / 64 MiB | Stateless CONNECT proxies have a small footprint; these defaults survive 500 concurrent tunnels with headroom. |
Tenant-Aggregate Capacity (single ActionsGateway)¶
| Resource | Target | Note |
|---|---|---|
| Active jobs (worker pods) | ≤ 250 | Conservative default governed by the platform-owned namespace ResourceQuota, maxWorkers, or the last priorityTiers threshold — whichever is most restrictive. Not rate-limit-bounded under the adaptive listener model; increase this ceiling by adjusting the namespace ResourceQuota and per-RunnerGroup concurrency controls. |
| Aggregate namespace ResourceQuota | 20 CPU / 40Gi memory / 50 pods | Conservative starting allocation. Platform-owned (set on the namespace, not the CR). Adjust against observed job CPU/memory profiles. |
Per-session memory & density (measured)¶
The peak-burst sizing above uses a deliberately conservative ~60 KiB/session. To
pin the AGC's actual per-session overhead — the figure behind the
density-versus-pod-per-runner claim — TestAGCPerSessionMemory
(cmd/agc/test/load/mem_test.go, make mem-profile) isolates it locally, with
no cluster, no real broker, and crucially no in-process broker stub (the stub
inflated the earlier ~127 KiB figure because its server side runs in the same
process).
Methodology. The probe drives the real multiplexing core
(listener.Multiplexer + agentpool.Pool + per-goroutine broker.Client) but
replaces the broker stub with memTransport, an in-process http.RoundTripper
that answers the OAuth/CreateSession/GetMessage calls with canned responses and
no server, socket, or per-session server-side state. GET …/message parks
the caller on its request context, so each of the 1,000 started listeners rests
in exactly one goroutine blocked in its long-poll — the steady idle-session
state. It then takes a three-point heap+stack differential: shared infra only →
plus N pooled agents and empty multiplexers → plus all N goroutines parked. The
last delta (mFull − mAgents) is the marginal cost of one more concurrent
session and excludes both the agent pool and the fake k8s client's retained
Secrets (an apiserver-side cost in production, not AGC memory).
Result (1,000 sessions, Go on darwin/arm64):
| Component | Per session |
|---|---|
| listener goroutine stack | ~8.1 KiB |
heap (broker.Client + live session state: sessionID, AES key, scoped logger) |
~4.1 KiB |
| AGC-only total (measured) | ~12.2 KiB |
The pre-registered agent struct (Ed25519 key + credentials, no JIT blob in this path) adds sub-KiB on top; the agent Secret itself is apiserver-resident in production. The measured ~12.2 KiB is ~5× below the ~60 KiB design estimate — the gap is the per-connection HTTP transport buffers that an active long-poll holds in production, which the in-process transport omits. The design estimate is therefore confirmed as a conservative upper bound.
Density versus pod-per-runner. Against ARC's Runner.Listener (~256 MiB
resident for the full .NET runtime):
256 MiB ÷ 60 KiB (conservative design figure) ≈ 4,400× ← the published "4,000×"
256 MiB ÷ 12.2 KiB (measured AGC structures) ≈ 21,000× ← floor excludes HTTP-conn buffers
The measurement confirms the published ~4,000× claim and shows it is conservative: even adding a generous per-connection HTTP-buffer allowance to the measured ~12 KiB keeps the per-session cost well under the 60 KiB the 4,000× figure assumes. We retain ~4,000× as the headline because it is the defensible, conservative number; the higher ratios above follow directly from the math but lean on assumptions the local probe cannot fully exercise.
These numbers should still be re-derived once two consecutive weeks of production telemetry are available. Treat the locally-measured figures as validated lower bounds on efficiency, not as a production-scale contract.
Validation status. The session-multiplexing core has been load-tested, and its per-session memory is now pinned. The in-process harness (
cmd/agc/test/load/, Q13;make load-test-quick) holds ~1,000 concurrent virtual sessions in a single AGC — a representative run sustained avg 998/1,000 with zero goroutine leak and 1.0 re-registrations per job (the single-use model under load). The faithful results from that tier are the sustained-session count, the no-leak guarantee, and the re-registration rate; it deliberately stubs the apiserver, registrar, and broker, so it does not speak to real apiserver/GitHub latency or worker-pod scheduling. The earlier ~127 KiB/session figure was an upper bound inflated by the in-process broker stub; the stub-free probe above (Q181) isolates the AGC's own structures at ~12.2 KiB/session, confirming the ~4,000× density claim is conservative. One caveat remains: the real-cluster, real-GitHub scale run — worker-pod scheduling and cross-tenant network at full concurrency — is still deferred. Operators should size against their own observed telemetry rather than treat these ceilings as proven.
← Glossary | Back to index | Next: Appendix B — Worker Isolation →