Skip to content

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.

Get started Migrating from ARC See the architecture

When the job is disrupted

ARCgives up on it. You re-run it by hand.
GAGre-runs it. Measured 15–26 s, preemption or drain.

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.

  • ResourceQuota is 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 AutoscalingRunnerSet only caps itself with maxRunners
    • no primitive for "GPU always keeps N slots"
    • cheap CPU pods exhaust the quota; big tests stall
  • 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:

Capability Quota-aware intake a job the quota cannot place is never claimed
Capability Automatic re-run a worker the cluster takes away comes back
Capability · partly shipped Isolation by default PSA, default-deny egress, and Kata micro-VM workers ship today; untrusted-PR network isolation does not yet
What that unlocks Shared capacity is safe to actually use tight per-tenant quotas, bin-packing, and preemptible nodes stop being risks
Less ops toil no manual reruns, no stuck-job tickets
Fewer pages cluster churn stops being an incident
Higher utilization hardware you already pay for gets shared
More throughput jobs finish per node-hour instead of stalling

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 capacityGate
off 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 workloadIdentity
an 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 RunnerTemplate
cluster-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 runs container: and services: 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:

  • 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.