
Direct answer: give every Mac runner a priority-pool label (hotfix, standard, or nightly), route jobs by that label, and size each pool with Little's Law against your real arrival rate. If you want that two-pool pattern already wired up, the Figment Managed Mac Team ($999/month, two M-series Macs, two CI runners, team setup, backups, monitoring, one hour of monthly support) matches it; a single-runner start fits the Managed Build Mac ($249/month). Exact chip, RAM, and disk are confirmed on the configuration quote.
A single-queue runner fleet makes every job first-come-first-served. When a nightly integration run occupies all three Mac runners, the hotfix build an on-call engineer just pushed sits 40 minutes behind it. The fix is rarely more hardware. It is labeling runners into separate pools so urgent work always has a lane, then sizing each pool deliberately.
Priority inversion happens when a low-priority job holds a resource a high-priority job needs. On a Mac CI fleet that looks like:
Named pools break the shared bottleneck. Hotfix jobs queue only behind other hotfix jobs; nightly jobs compete only with other nightly jobs. The arrival rates and service times are different enough that mixing them is what causes the wait.
Give each Mac runner at least two labels: a capability label (macos, xcode-26) and a priority-pool label (hotfix, standard, or nightly). Do not assign a runner to more than one pool; whichever job arrives first will take it and the separation is gone.
A three-runner fleet might be allocated like this:
| Runner | Pool | Typical jobs |
|---|---|---|
| mac-runner-01 | hotfix | Emergency patches, blocker PRs |
| mac-runner-02 | standard | Regular PR builds, unit tests |
| mac-runner-03 | nightly | Full integration suites, archiving |
With two runners, assign one to hotfix and one to standard, and schedule nightly jobs on the standard runner during low-PR hours.
Add custom labels when you register the runner, then reference them in runs-on. A self-hosted runner must match every label in the runs-on array to be eligible, per the GitHub Actions self-hosted runner docs.
# Registration (run on the Mac itself)
./config.sh --url https://github.com/ORG/REPO \
--token "$RUNNER_TOKEN" \
--labels self-hosted,macOS,xcode-26,hotfix
# .github/workflows/hotfix.yml
jobs:
build:
runs-on: [self-hosted, macOS, hotfix]
steps:
- uses: actions/checkout@v4
- run: xcodebuild build -scheme "$APP_SCHEME" -destination 'generic/platform=iOS'
# .github/workflows/nightly.yml
jobs:
integration:
runs-on: [self-hosted, macOS, nightly]
steps:
- uses: actions/checkout@v4
- run: xcodebuild test -scheme "$APP_SCHEME_TESTS" -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest'
Replace $APP_SCHEME and $APP_SCHEME_TESTS with the scheme names from your own xcodebuild -list output; do not copy a placeholder scheme into production. Both jobs must stop on failure (the default); a failed build is not a benchmarked run.
Tag the agent, then target the queue in the pipeline. Agent targeting in Buildkite is a two-tier model: tags are set in the agent configuration and referenced under agents: in the step, per the Buildkite defining steps docs.
# /etc/buildkite-agent/buildkite-agent.cfg (mac-runner-01)
tags="os=macOS,xcode=26,queue=hotfix"
steps:
- label: "Hotfix build"
command: "scripts/build.sh"
agents:
queue: hotfix
os: macOS
Tag the runner on registration, then require the tags in the job:
# .gitlab-ci.yml
build-hotfix:
tags:
- mac
- hotfix
script:
- xcodebuild build -scheme "$APP_SCHEME" -destination 'generic/platform=iOS'
Little's Law gives the minimum number of runners needed to keep the queue empty on average:
N = λ × W
N = runners required at zero steady-state queueλ = arrival rate (builds per hour)W = average service time per build (hours)That number is a floor. Round up, then add at least one runner per pool as spike headroom.
| Pool | Peak builds/hr (λ) | Avg duration (W) | N = λ × W | Recommended |
|---|---|---|---|---|
| Hotfix | 2 | 0.17 h (10 min) | 0.34 → 1 | 1 (dedicated) |
| Standard | 10 | 0.20 h (12 min) | 2.00 → 2 | 2–3 |
| Nightly | 1 | 1.00 h (60 min) | 1.00 → 1 | 1 |
Check the math: 10 builds/hour × 12 minutes = 120 build-minutes per hour = 2.0 runner-hours per hour, so 2 runners is the floor and 3 is the realistic allocation once variance is included.
Fill in your own numbers:
runners_min = ceil( peak_builds_per_hour × (avg_build_minutes / 60) )
runners_recommended = runners_min + 1
When λ × W exceeds N, jobs wait. Your levers are: cut arrivals (defer nightly work), shorten W (caching, parallel test shards), or raise N (add a machine to the pool).
Linux CI services spin up ephemeral runners in seconds. Mac hardware does not. Physical Mac provisioning at Figment is request-based until supply is confirmed, and virtualization has its own constraints, so the practical pattern is a fixed fleet with pool labels protecting reserved capacity. If the standard pool bursts, the simplest release valve is scheduling the nightly runner to join the standard pool for a defined window; CI systems that support dynamic labels can swap pool tags without a full reinstall.
The Figment $29 virtual Intel macOS environment can run compatible Intel Mac toolchains, including supported Xcode 26 or earlier releases on Intel, so it is a reasonable standard or nightly pool member for Intel-compatible targets. ARM-only execution, visionOS, or Xcode 27 belong on the M-series tier.
Queue wait time is the actionable signal. A reasonable starting alert is median hotfix wait over 2 minutes or standard wait over 10 minutes; tune those to your own data.
created_at to started_at from the workflow-runs REST API, grouped by runner label.Log wait time inside the job itself and chart it weekly; creeping saturation shows up as a rising median long before anyone files a ticket.
The Managed Mac Team plan is $999/month and, as listed, covers two M-series Macs, two CI runners, team setup, backups, monitoring, and one hour of monthly support. That slots directly into a hotfix + standard two-pool layout, with nightly jobs scheduled on the standard runner off-hours. For a single-runner start, the Managed Build Mac at $249/month covers one M-series Mac with CI setup, backups, monitoring, and 30 minutes of monthly support. Exact chip, RAM, and disk are agreed on the configuration quote; the M-series line starts at $95 and physical-tier availability is confirmed before purchase.
When Little's Law shows your standard pool consistently needing three or more runners, or nightly jobs routinely bleed into business hours, request a third-runner configuration. Fleets beyond that are quoted individually with capacity and delivery agreed upfront.
For related reading, see CI artifact storage and retention on a Mac, incremental Xcode builds via derived data cache, and remote Xcode from Windows.