Comparison · ARC alternative
Why GitHub Actions Gateway over ARC?¶
Actions Runner Controller (ARC) struggles with one job: many runner sets, many tenants, one shared cluster, each tenant safely capped by its own ResourceQuota. GitHub Actions Gateway (GAG) is built for that job.
When the job is disrupted
The problem ARC leaves you with¶
All four trace to one root, and it is not a missing feature. ARC models a cluster with one owner, so it has no primitive separating what the platform owns from what a tenant owns. That is a reasonable product for a single-tenant cluster, and the same gap from four angles once teams share one.
-
ResourceQuotais unsafe
A quota-blocked or evicted job can't recover on its own:
- claimed before the quota is known, so the runner cannot start
- its single-use registration is already spent when quota is found
- ARC retries every 30 s, recycling every 10 min (0.13.1)
- the job reads assigned, so the 24 h queue timeout never fires
-
Critical jobs starve
No way to reserve capacity for expensive runners:
- each
AutoscalingRunnerSetonly caps itself withmaxRunners - no primitive for "GPU always keeps N slots"
- cheap CPU pods exhaust the quota; big tests stall
- each
-
Listener pods pile up
One always-on listener pod per scale set, running 24/7:
- a pod slot and a pod IP each
- held alive just to long-poll GitHub
- the count tracks runner sets, not job volume
-
Platform team is the bottleneck
Every tenant is a manual checklist:
- namespace, quota, RBAC, scale sets, NetworkPolicies, egress
- per-team setup; every later change is a ticket
What changes with GAG¶
Three capabilities that only pay off together, and what they unlock:
A tenant declares only namespaced resources; the GMC provisions the rest inside a platform-owned quota it cannot write, with no cluster admin after the install (the object set). The cost model runs the utilization argument in numbers, but a benchmark at scale is on the roadmap and not yet done: treat those figures as a model, not a measurement.
GAG vs ARC (scale-set mode)¶
This is not a protocol argument. GAG acquires jobs with the same runner-scale-set protocol ARC uses, shipped as the v2 default, so every row below is additive rather than a different-architecture trade-off. What differs is what surrounds that shared core.
| Capability | ARC (scale-set mode) | GitHub Actions Gateway |
|---|---|---|
| Runner-scale-set acquisition (single-acquirer, no fan-out) | yes | yes, by default v2 |
| Ephemeral, single-use runner pods | yes | yes |
| Custom runner pod template & image | yes | yes |
| Workers scale to zero between jobs | yes, by default | yes, by default |
Safe under a per-tenant ResourceQuota |
claims first, then discovers the quota burns a single-use runner registration every 10 min, indefinitely; the job reads as assigned, so GitHub's 24 h queue timeout never fires |
never claims a job it can't place reads quota headroom before claiming, so nothing is spent and the job stays visibly queued until there is room |
| Auto-re-run jobs disrupted mid-flight (eviction / preemption / drain) | no re-run mechanism exists the runner registration is removed and the job is given up on; 0.13.1 recovers the pod, never the job |
re-runs itself in seconds 15–26 s measured for a preemption or drain; evictions, drains and hand-deleted workers all covered, under a per-run budget |
| Stop claiming jobs when the cluster can't place the worker | acquires every available job unconditionally the seat exists; no cluster state is consulted in it |
opt-in capacityGateoff by default. Bounds the rate of wasted claims to roughly one per pendingPodDeadline; it does not eliminate the first |
| Guaranteed floor for critical runner types | no per-quota primitive | priority tiers per runner set |
| Throttle the rate new workers start (anti-stampede) | no per-set start-rate control its throttles are controller-wide and meter API calls, not worker onset |
opt-in per-set creation-rate limit for shared-egress onset (NAT, firewall, VPN) |
| Per-tenant dedicated egress IPs | points at a proxy you already run provisions no pool, manages no lifecycle, gives no per-tenant addresses |
provisioned per-tenant pool, live-validated on GKE 2026-07-13: one distinct, stable per-tenant NAT IP. A stable source IP needs Cilium Egress Gateway or cloud NAT beneath the pool, both specified v2 |
| GitHub App private key kept out of the cluster | the listener reads the key either way the PEM sits in githubConfigSecret and is copied into the generated listener config Secret too; opt-in Azure Key Vault keeps it out of etcd, but the listener still fetches the key itself |
v2 opt-in workloadIdentityan external signer signs the App JWT, so no App key exists in the cluster in any form. Its default githubApp member is the same in-cluster PEM as ARC's |
| Listener footprint at rest | one always-on listener pod per scale set, each holding a pod IP they run in the controller's namespace, so the cost lands on platform pod density rather than the tenant's quota |
1 shared pod, ~12 KiB per listener session |
| Per-tenant utilization metrics | opt-in, per scale set ships commented out; carries a namespace label but nothing aggregates across sets, and no quota-headroom series |
per tenant and per runner set plus a dashboard and 20 alert rules as code |
| Right-size runner resources from measured usage | no feedback loop, so runner resources stay a guess |
v2 measured recommendations in RunnerSet status+ opt-in profiles that auto-apply them at pod build ( Binpack/Throughput/NodeShare), a SizingDrift condition, and per-job peak metrics |
| Cross-tenant fleet health view (platform admin) | one sample dashboard, no alert rules nothing aggregates across scale sets or tenants |
single-pane fleet rollups degraded, egress-stale and quota per gateway, plus a platform dashboard |
| Multiple gateways per namespace | multiple AutoscalingRunnerSets |
v2 multiple scoped gateways per namespace |
| Reusable runner pod templates | template inlined per AutoscalingRunnerSet |
v2 shared RunnerTemplatecluster-wide ClusterRunnerTemplate |
Every GAG capability above is available today. Rows marked
v2 need the actions-gateway.com/v2beta1 API,
which is where new tenants start.
When the ARC column was measured, and why that matters
Measured against ARC gha-runner-scale-set 0.14.2 (released 2026-05-22)
and the master branch, on 2026-08-06, and the credential row on
2026-08-09, by reading the controller source, the chart values, and the
release notes rather than the documentation.
ARC moves, and an undated comparison rots into a false one. Two rows here changed at datable releases: 0.13.1 (2025-12-23) changed how a quota-blocked pod creation is retried, and 0.14.0 (2026-03-19) added multi-label scale sets, which GAG does not have. If you are evaluating on a later ARC than the one stamped above, re-check the column rather than trusting it, and tell us what changed.
Two rows with fine print¶
Auto-re-run covers disruption, never failure. Eviction, preemption, a drain
and a stray kubectl delete pod all come back, on both acquisition tiers. A job
that failed and a run you cancelled never do. Nor do workers the reaper took,
with one exception: a worker reaped while still Pending is re-run on the
classic tier once capacity returns.
The full boundary.
Right-sizing is structural, not a feature race. An ephemeral pod runs one job and lives minutes, so stock Vertical Pod Autoscaler cannot size it. The loop only closes inside the controller that builds the pods. Appendix D.7.
Where ARC is ahead¶
Some of it is capability, not only maturity. Measured 2026-08-06. Each gap below is tracked, and what closes it and when is on the public roadmap; the support entitlement is the one we do not plan to match.
- A GitHub Support entitlement, covering ARC installed via the official Helm charts, GitHub Enterprise Server 3.9 and later. GAG has none. Read the scope exclusions before relying on it: Kubernetes orchestration, policy application, and template customization are explicitly out of scope, which is much of what a multi-tenant platform team actually pages about.
- Multi-label scale sets since 0.14.0. A workflow using
runs-on: [linux, gpu]needs one edit per target to move to GAG, which admits exactly one label per runner set. containerMode: kubernetes, which runscontainer:andservices:steps as separate pods with a provisioned volume. GAG runs one worker pod per job, so that path is Docker-in-Docker (under Kata, unprivileged) rather than a non-privileged pod-per-step model.- GitHub runner groups (
runnerGroup), the forge-side control over which repositories may target a runner set. - GHES that is actually tested. GAG serves GHES gateways and marks both of its GHES features untested against a real appliance.
The maturity gap is real too: ARC is GA and widely deployed, while GAG's v2 API
has only just reached beta (v2beta1, its first stability contract). That is why
the v1 to v2 migration runs on a committed, documented schedule with a working
gag-migrate tool. The discipline is the
"won't strand you" signal while the track record accumulates.
One thing is not a differentiator either way: both ride the same
Public-Preview runner-scale-set protocol, through the same
actions/scaleset client library.
Secure by default¶
Built for shared clusters running other teams' code: the multi-tenant hardening ships as reconciled defaults, not a post-install project.
-
Risk reduction
Untrusted job code is boxed in by default:
baselinePod Security Admission per namespace- Default-deny network: DNS + own proxy only
- App keys read-only; never in env, never cached
- Or opt in to no App key in the cluster at all
- Controller writes confined to tenant namespaces
- Sharing a proxy across namespaces needs the owner's consent
-
Lower operational cost
What you'd hand-build around ARC, reconciled from one CR:
- NetworkPolicies · PSA · RBAC · egress
- No Kyverno/OPA required: in-tree PodSecurity
- Kept in sync as tenants come and go
-
Ready out of the box
Secure by default; looser is an explicit opt-in:
- Default-deny ingress, cluster-only DNS
- Per-tenant egress IPs, mutual-TLS metrics
- Signed images, SBOM, and SLSA provenance
- Three validated worker templates, one
kubectl apply -k
Sandboxing is not the runtimeClassName field¶
Both GAG and ARC can set one. The differentiator is the layer underneath.
Kata bounds the kernel, not the pod network, so cloud metadata still answers from inside the guest. GAG's default-deny NetworkPolicies close that path.
GAG's own CI runs this way, building a kind cluster inside a worker pod
with zero privileged: true (how, and what Kata does not buy
you).
Not yet a claim about untrusted pull requests, which need the egress work on the roadmap.
Threat model and abuse-response playbooks: Security, Security operations.