Git, CI/CD & GitOpsC · Runner CapacityAutoscaling
Runner autoscaling with ARC — Kubernetes-based scale to zero
What you'll learn
- Inspect an ARC deployment's autoscaling state with kubectl
- Distinguish HPA-driven scaling from listener-driven scaling in ARC
- Recognise the role of minRunners, maxRunners, and replica limits
- Identify the metrics that ARC and Kubernetes expose for autoscaling decisions
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
ARC’s value is the ability to scale runner pods to zero at
night and back up to dozens at peak. The control plane that
makes this work is a Kubernetes controller reconciling an
EphemeralRunnerSet against the forge’s queue depth. The
operational question is: when something is wrong with
scaling, what does the team look at?
Inspecting an ARC deployment
The three kubectl commands that matter when investigating scaling behaviour:
ARC_NS=actions-system
kubectl get actionsrunnersets -n "$ARC_NS"
This lists every EphemeralRunnerSet (or RunnerSet, on
the older ARC API) in the cluster, with name and age. The
output tells the operator which runner pools exist. It does
not yet tell them how the pool is configured.
kubectl get hpa -n "$ARC_NS"
HorizontalPodAutoscalers do not directly scale ARC runners
- the listener does that. But ARC’s autoscaling behaviour is
constrained by
maxRunnerson theEphemeralRunnerSetand by the cluster’s capacity. The HPA view tells the operator whether the cluster can scale the workload class that ARC creates, which matters when the bottleneck is cluster capacity rather than ARC’s configuration.
kubectl describe actionsrunnersets -n "$ARC_NS" \
prod-runners
This shows the current and desired replica counts, the listener connection status, and any conditions the controller has reported. “Progressing: False” with a reason of “RunnerScaleSetReconcilerError” is the typical signal that the listener cannot reach the forge.
The two scalers
ARC’s autoscaling is the composition of two scalers:
flowchart TB
Q["Forge queue\nworkflow_job events"] --> L["ARC listener"]
L -->|"desired replicas"| ERS["EphemeralRunnerSet"]
ERS -->|"creates pods"| K["Kubernetes API"]
K -.->|"capacity check"| HPA["HPA / cluster autoscaler"]
HPA -.->|"nodes"| K
- Listener-driven scaling. The ARC listener watches the
forge’s job queue and writes the desired replica count
into the
EphemeralRunnerSetstatus. This is the primary scaler - it reacts to actual job arrivals. - Capacity-driven scaling. The Kubernetes cluster must
have nodes to schedule the pods the listener asks for. If
the cluster is full, the pods stay
Pendingand the queue grows. The HPA and cluster autoscaler respond to resource pressure; they do not respond to job arrivals.
A common confusion is treating ARC scaling as if it were HPA scaling. It is not. ARC’s listener is the queue-aware scaler; HPA is the resource-aware scaler that backstops it.
The operational knobs
The EphemeralRunnerSet exposes three knobs the operator
sets deliberately:
minRunners. The floor.0means scale to zero when idle;1keeps one warm;NkeepsNwarm. Cost versus cold-start latency is the trade.maxRunners. The ceiling. The listener will not request more than this. Set it to the cluster’s surge capacity for the runner class, not to a number that pleases a budget.- Runner template resource requests/limits. The pod
template’s
resources.requestsdetermine how the scheduler places pods;resources.limitsdetermine the pod’s runtime ceiling. A pod that exceeds its limits is throttled or killed, which is rarely what the team wants during a build.
What to alert on
Four signals cover most ARC operational issues:
- Listener connected. The
EphemeralRunnerSetconditions report this. AFalsecondition with reasonListenerNotConnectedis the alert. - Pending runner pods.
kubectl get pods -n "$ARC_NS"withPendingfor more than 60 seconds means the cluster cannot place the pods. - Queue depth. From the forge’s metrics - jobs waiting
for a runner. A depth that exceeds
maxRunnersfor more than 5 minutes means the pool is permanently saturated. - Cold-start latency. The time from
workflow_job queuedto runner-pod-ready. A regression here is the first sign that the runner image is growing.
When autoscaling is not enough
ARC’s listener-driven scaling handles burst within the
configured maxRunners. It does not handle:
- Cluster capacity. If the cluster is full of other
workloads, ARC cannot schedule new pods regardless of
maxRunners. - Image-pull time. A 5 GB runner image delays every cold start. The fix is at the image layer.
- Job duration. A job that takes an hour is not made faster by autoscaling. The fix is at the workload layer.
Production discipline
- Set
minRunnersfrom cold-start budget, not from intuition. Measure cold-start first; then decide. - Set
maxRunnersfrom cluster surge capacity. AmaxRunnershigher than the cluster can deliver is a number, not a plan. - Alert on listener health first. The listener is the bridge; if it is broken, nothing else scales.
- Separate pools by trust level. A
prodand anuntrustedpool with different IAM, network, and templates. - Trim the runner image. A 1 GB image cold-starts in 10 seconds; a 5 GB image cold-starts in 50.
Cross-course references
- Git, CI/CD & GitOps - Part XL (Runners) covers the ARC architecture this lesson extends.
- Kubernetes for Production Sysadmins - Part XXII (HPAMath) covers HPA scaling in depth.
- Observability for Production Sysadmins - Part VIII (K8sObs) covers the alerting patterns used here.
Quiz
Knowledge check · 4 questions
Q1. Which kubectl command directly reports ARC's desired-vs-actual replica count and listener connection status?
Q2. ARC scales runner pods in response to CPU utilisation via the HorizontalPodAutoscaler.
Q3. Name the three operational knobs on an EphemeralRunnerSet that govern autoscaling behaviour.
Q4. Diagnose why an ARC pool with maxRunners: 50 still queues jobs at peak.
Team T configured their prod pool with maxRunners: 50 and minRunners: 0. At the 10am peak, queue depth grows to 40 jobs and wait time climbs to 12 minutes. The cluster has 20 nodes, each with 8 vCPUs and 32 GB RAM. Runner pods request 2 vCPUs and 4 GB RAM.
Passing score: 75%. Answers are checked in this browser.