Comparison · ARC alternative
Why GitHub Actions Gateway over ARC?¶
Actions Runner Controller (ARC) scale-set mode struggles with one job: running many runner sets, for many tenants, in one shared cluster — cost-effectively, with each tenant safely capped by its own ResourceQuota. GAG was built for exactly that, without giving up the self-service that makes a shared cluster worth running.
When a worker is evicted or blocked by a full ResourceQuota
Failed and the job sits in GitHub's queue until someone reruns it by handThe problem ARC leaves you with¶
The failures compound, but they all trace back to one root: ARC's poor fit with
ResourceQuota makes per-tenant quotas unsafe — and unsafe quotas are what block
letting tenants run their own runners.
-
ResourceQuotais unsafe
A quota-blocked or evicted job can't recover on its own:
-
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 memory piles up
One .NET listener pod per scale set, running 24/7:
- ~256 MiB resident + a cluster IP each
- held alive just to long-poll GitHub
- 10 scale sets ≈ 2.5 GiB before a job runs
-
Platform team is the bottleneck
Every tenant is a manual checklist:
- namespace, quota, Role-Based Access Control (RBAC), scale sets, NetworkPolicies, egress
- per-team setup; every later change is a ticket
What changes with GAG¶
GAG vs ARC (scale-set mode)¶
GAG acquires jobs with the same runner-scale-set protocol ARC uses — a single acquirer per runner set, capacity-gated assignment, no many-acquirers fan-out — and it is the shipped default in the v2 API. So the comparison below is capability-for-capability against ARC's own model: every GAG row is additive, not a different-architecture trade-off. The difference is what surrounds the shared acquisition core — quota safety, priority tiers, per-tenant egress, and control-plane footprint.
| 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, with minRunners: 0 |
yes, by default |
Safe under a per-tenant ResourceQuota |
quota-blocked jobs stall; manual cleanup + rerun | auto lock-cancel + re-queue |
| Guaranteed floor for critical runner types | no per-quota primitive | priority tiers per runner set |
| Throttle the rate new workers start (anti-stampede) | only maxRunners caps the count — a burst starts all at once |
opt-in scaleUp creation-rate limit per setfor shared-egress onset (NAT / firewall / VPN) |
| Per-tenant dedicated egress IPs | shared cluster egress | per-tenant proxy pool v2 proxy optional |
| Listener memory, 10 runner sets at rest | ~2.5 GiB across 10 pods | ~600 KiB in 1 shared pod |
| Per-tenant utilization metrics | scale-set metrics, not tenant-scoped | Prometheus per tenant + group job counts in kubectl get; ready-to-apply tenant dashboard + alerts as code |
| Cross-tenant fleet health view (platform admin) | controller + per-scale-set metrics, aggregated by hand; no bundled dashboard | single-pane GMC fleet rollups degraded / egress-stale / quota per gateway, + 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 capability above is available today.
Onboarding: start on v2
New tenants should onboard on the recommended v2 API
(actions-gateway.com/v2alpha1 — a decomposed ActionsGateway + RunnerSet +
RunnerTemplate, with an optional standalone EgressProxy); the rows marked
v2 are v2-only. The single-CR v1alpha1 shape
shown below is still fully served but
deprecated — see the
v1 → v2 migration guide and the
getting-started walkthrough for the v2 object set.
The numbers behind these claims
For limits and Service Level Objectives, see Appendix A — Capacity Targets & SLOs; for the utilization-and-cost argument, Appendix F — Cost model.
Where GAG is behind ARC
It's maturity, not capability. ARC is GA and widely deployed; GAG's
recommended v2 API is still alpha and rides a Public-Preview runner-scale-set
protocol. That is precisely why the v1 → v2 migration is handled 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.
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 (PSA) per namespace- Default-deny network — DNS + own proxy only
- App keys read-only; never in env, never cached
- Controller writes confined to tenant namespaces
-
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 + Software Bill of Materials (SBOM) + Supply-chain Levels for Software Artifacts (SLSA) provenance
For the full threat model, per-profile controls, and the abuse-response playbooks, see Security and Security operations.
Composable building blocks, not one giant CR¶
A tenant still declares only namespace-scoped resources, and the Gateway Manager
Controller (GMC) provisions the controller, proxy pool, RBAC, and network policies
to match — all within the platform-owned ResourceQuota the GMC never creates
or mutates, with no per-tenant cluster-admin after the initial install. What
changed with the recommended v2 API is that the single-CR monolith is
decomposed into small, reusable kinds — and that decomposition is a
differentiator ARC's inlined, per-scale-set model structurally can't express:
-
Reuse the pod shape
One
RunnerTemplate— or cluster-wideClusterRunnerTemplate— is referenced by everyRunnerSet. ARC inlines the pod template into eachAutoscalingRunnerSet, so N runner types means N copies to keep in sync. -
Clean ownership boundary
Platform owns the quota, the
PriorityClassallowlist, and cluster templates; the tenant composesRunnerSets within them. ARC has no primitive to separate platform-owned from tenant-owned concerns. -
Egress on purpose
A standalone
EgressProxyis referenced by the gateway (or perRunnerSet), or dropped entirely for direct — stillNetworkPolicy-restricted — egress. ARC has no per-tenant egress primitive at all. -
Many gateways, one namespace
Multiple scoped
ActionsGateways coexist in a namespace, each with its own GitHub binding and runner sets — not one CR that must own everything.
The v2 object set below is feature-equivalent to the legacy single-CR example — a proxied gateway with a GPU runner set (priority tiers) and a Linux runner set:
apiVersion: actions-gateway.com/v2alpha1
kind: EgressProxy # (1)!
metadata:
name: team-a-egress
namespace: team-a
spec:
minReplicas: 2
maxReplicas: 10
---
apiVersion: actions-gateway.com/v2alpha1
kind: RunnerTemplate # (2)!
metadata:
name: default
namespace: team-a
spec:
podTemplate:
spec:
containers:
- name: runner
---
apiVersion: actions-gateway.com/v2alpha1
kind: ActionsGateway # (3)!
metadata:
name: team-a-gateway
namespace: team-a
spec:
credentials:
type: GitHubApp
githubApp:
name: my-github-app # name-only Secret ref in this namespace
githubURL: https://github.com/team-a-org
defaultProxyRef:
name: team-a-egress # every RunnerSet inherits this unless it sets proxyRef
---
apiVersion: actions-gateway.com/v2alpha1
kind: RunnerSet
metadata:
name: gpu
namespace: team-a
spec:
gatewayRef: { name: team-a-gateway }
templateRef: { name: default } # (4)!
runnerLabels: ["gpu", "self-hosted"]
maxListeners: 10
priorityTiers: # (5)!
- priorityClassName: runner-critical
threshold: 5
- priorityClassName: runner-standard
threshold: 20
---
apiVersion: actions-gateway.com/v2alpha1
kind: RunnerSet
metadata:
name: linux
namespace: team-a
spec:
gatewayRef: { name: team-a-gateway }
templateRef: { name: default }
runnerLabels: ["linux", "self-hosted"]
maxWorkers: 30
- Optional. A standalone per-tenant egress proxy pool, Horizontal Pod Autoscaler
(HPA)-managed between these bounds; all GitHub traffic exits through it on
dedicated IPs. Drop it (and
defaultProxyRef) for direct, stillNetworkPolicy-restricted egress — collapsing the minimum to three objects. - A reusable pod shape referenced by both
RunnerSets below viatemplateRef. Define it once; a cluster-scopedClusterRunnerTemplateshares one shape across every namespace. The Pod Security Admission level is a namespace label in v2, not a CR field — all gateways in a namespace share one level. credentials.githubApp.namereferences aSecretin this namespace holding the GitHub AppappId,installationId, andprivateKey. The GMC watches the reference name, not the Secret contents — see credential rotation.WorkloadIdentityis the opt-in no-PEM credential member.- Both runner sets reference the same
RunnerTemplate. There is noResourceQuotafield on any of these CRs — the single quota every runner set shares is platform-owned, set on the namespace by the platform admin, so it is a real cap the tenant cannot raise. Priority tiers decide who wins when it is contended. - The first 5 GPU pods get the higher-priority
PriorityClass; the next tier bursts opportunistically; the final threshold caps total concurrency. ThepriorityClassNamevalues must be on the platform's allowlist (the GMC--allowed-priority-classesflag), and whether a tier preempts is set on the platform-ownedPriorityClassobject — a tenant cannot name a class that evicts other tenants' pods.
The legacy single-CR v1alpha1 shape — which expresses this whole gateway in one
ActionsGateway CR — is still fully served but
deprecated; see the
getting-started walkthrough
for it and the v1 → v2 migration guide to move
across without changing how your jobs are acquired.
Ready to try it? Follow the getting-started guide. Already running ARC? The Migrating from ARC guide maps every concept above onto GAG and walks one runner group across with zero downtime.