Git, CI/CD & GitOpsC · Runner CapacitySizing
Queueing and wait times — what happens when jobs exceed capacity
What you'll learn
- Explain what happens to queued jobs when arrival rate exceeds runner capacity
- Recognise the non-linear relationship between utilisation and wait time
- Identify the metrics that signal saturation before SLAs are breached
- Apply queueing concepts to decide when to scale the pool vs. when to cap concurrency
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
A runner pool can be sized correctly for the average workload and still fail during the peak. The reason is the shape of the queueing curve. As utilisation rises, wait time does not rise proportionally - it rises exponentially, and by the time wait time is visible in user-facing metrics the pool is already deep in the danger zone.
The utilisation curve
For a single queue with random arrivals and variable service times, average wait time is approximately:
wait ≈ (utilisation² / (1 - utilisation)) × avg_service_time
A pool at 50% utilisation has almost no queue. At 80% the
average wait time is 0.64 / 0.20 = 3.2 times the service
time. At 90% it is 0.81 / 0.10 = 8.1 times. At 95% it is
0.9025 / 0.05 = 18 times.
flowchart LR
U["Utilisation"] -->|low| W["Wait near zero"]
U -->|0.8| W2["Wait 3x service"]
U -->|0.9| W3["Wait 8x service"]
U -->|0.95| W4["Wait 18x service"]
The curve is the trap. Doubling the arrival rate at low utilisation barely moves wait time. Doubling it near saturation breaks the pool.
Recognising saturation
Saturation has three observable signals that arrive in this order:
- Queue depth rises. The forge’s queue (workflows waiting for a runner) holds jobs instead of dispatching them immediately.
- Wait time rises.
time_queued_to_starton individual jobs grows from seconds to minutes. - SLA breaches appear. PRs that promised a 10-minute CI turnaround take 25.
The first signal is the cheap one. Queue depth is observable from the forge’s metrics before any user notices. The second signal is the one the team notices. The third signal is the one the manager notices.
Capping concurrency vs scaling
When saturation appears, the team has two levers:
- Scale the pool. Add runners. The fastest response on
ARC is raising
maxRunners; on hosted runners, raise the per-org runner limit. - Cap concurrency. Limit how many jobs a single workflow or PR can hold. protects the pool from a runaway build matrix.
ARC_NS=actions-system
kubectl get actionsrunnersets -n "$ARC_NS" \
-o custom-columns='NAME:.metadata.name,MIN:.spec.minRunners,MAX:.spec.maxRunners'
This shows the current minRunners/maxRunners for each
runner set. The fix for a saturated pool is almost always
maxRunners, not minRunners.
What the queue does to retries
A subtle failure mode: when the pool is saturated, failed jobs retry into a queue that is already long. The retry adds to the arrival rate, deepening the saturation. This is retry-induced congestion, and it turns a 5-minute blip into a 30-minute outage.
The mitigation is two-sided. First, exponential backoff on retries so failed jobs do not re-enter the queue immediately. Second, a circuit breaker that stops retries when the queue depth exceeds a threshold - failed jobs sit in a failed state instead of a queued state until the pool recovers.
Production discipline
- Alert on queue depth, not on wait time. Queue depth is the leading indicator; wait time is the lagging one.
- Treat 80% utilisation as the operation ceiling. Push past it only with explicit sign-off and a documented reason.
- Retries must back off. A retry that re-enters the queue immediately is a self-inflicted attack.
- Scale on
maxRunners, notminRunners. The ceiling is what protects SLAs. - Run a saturation drill. Take the pool to 90% utilisation on a test day and observe the curve.
Cross-course references
- Git, CI/CD & GitOps - Part LXIII (Observability) covers the queue-depth metric in the larger observability picture.
- Observability for Production Sysadmins - Part V (QueueMath) covers Little’s Law and the utilisation curve in depth.
- Kubernetes for Production Sysadmins - Part XXII (HPAMath) covers the same curve applied to pod autoscaling.
Quiz
Knowledge check · 4 questions
Q1. A runner pool operates at 80% utilisation. Average job duration is 5 minutes. Approximately how long does a queued job wait on average before starting?
Q2. Saturation is best detected by waiting for users to report slow CI, because wait time is the first observable signal.
Q3. Name the three observable signals of saturation in order, from cheapest to most expensive.
Q4. Diagnose why a pool at 70% utilisation is queueing jobs after a dependency upgrade that doubled build time.
Team T's pool was sized for 16 concurrent jobs at 5-minute average duration, operating at 70% utilisation. They upgraded a dependency that doubled build time to 10 minutes. Queue depth and wait time both rose sharply. Engineers report CI taking 25-30 minutes for a 5-minute job that should take 5.
Passing score: 75%. Answers are checked in this browser.