Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Manage Build Queues Across Mac CI Runners

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.

How a Single Queue Creates Priority Inversion

Priority inversion happens when a low-priority job holds a resource a high-priority job needs. On a Mac CI fleet that looks like:

  • A nightly 500-test run claims the only idle runner seconds before a P0 hotfix lands.
  • A Dependabot update saturates the fleet on Monday morning.
  • An overnight simulator suite overruns standup and blocks every PR build.

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.

Labeling Runners Into Pools

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:

RunnerPoolTypical jobs
mac-runner-01hotfixEmergency patches, blocker PRs
mac-runner-02standardRegular PR builds, unit tests
mac-runner-03nightlyFull 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.

Configuring Priority Queues in GitHub Actions, Buildkite, and GitLab

GitHub Actions

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.

Buildkite

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

GitLab CI

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'

Runner Sizing Worksheet Using Little's Law

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.

Worked example

PoolPeak builds/hr (λ)Avg duration (W)N = λ × WRecommended
Hotfix20.17 h (10 min)0.34 → 11 (dedicated)
Standard100.20 h (12 min)2.00 → 22–3
Nightly11.00 h (60 min)1.00 → 11

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

Autoscaling vs Fixed Mac Fleets

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.

Monitoring Queue Depth

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.

  • GitHub Actions: compare created_at to started_at from the workflow-runs REST API, grouped by runner label.
  • Buildkite: per-queue wait time and job age are exposed in the dashboard and can be scraped through the metrics API.
  • GitLab: pipeline analytics shows per-tag runner queue-time trends.

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.

Mapping Pools to a Figment Plan

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.

Start on Figment Mac hosting → · See the Mac hosting plans


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms