A growing iOS build queue is delaying reviews, but the team cannot tell whether it needs another Mac node or better task routing.
Do not convert developer headcount into Mac nodes. Use real queue and run records to identify eligible concurrency, job duration, toolchain constraints, and signing restrictions; estimate capacity by workload pool, then use a pilot to decide whether to expand or rent.
For enterprise IT leaders planning Mac resources and preparing a purchase or rental request.
For platform engineers routing GitHub Actions self-hosted Mac Runner jobs and managing queues.
For FinOps leaders building a capacity and budget model that can be recalculated from actual usage.
Enterprise iOS CI Mac capacity estimation starts with workload evidence
A node estimate is only useful when it reflects jobs that can actually run on that node. GitHub Actions routes a job to an eligible runner based on its requirements, including labels and runner group access. If no matching online runner is idle, the job waits in the queue. That behavior is documented for self-hosted runners in GitHub’s runner reference and its workflow routing guidance.
Build your estimate around those eligible jobs, not every workflow in the repository. A workflow may include linting, static checks, or other steps that do not require macOS. Routing such work to a suitable non-Mac runner keeps it out of the Mac capacity calculation. Conversely, an iOS job that requires Xcode cannot be counted as served by a general-purpose runner simply because the workflow is shared.
Use team records to capture the evidence behind each estimate:
- Job arrival and start times, so you can distinguish queue delay from execution time.
- Job duration and outcome, grouped by workflow and job type.
- Runner labels, groups, and online availability when the job became eligible.
- Concurrent eligible jobs during normal periods and release or regression bursts.
- Toolchain, architecture, simulator, and signing requirements that restrict placement.
GitHub’s workflow_job webhook documentation describes job lifecycle events that can help you collect queue and execution records. Treat your own CI history as the source for workload volume and duration. GitHub’s platform rules explain routing behavior; they do not prescribe how many Macs your organization needs.
PR checks and routine builds: route first, then size the pool
PR checks, commits, and main-branch builds often appear to be one workload, but they can have different arrival patterns and priorities. A quick PR check may need prompt feedback, while a main-branch build may tolerate a longer wait. Start by grouping jobs by workflow purpose and observing when eligible work arrives, how long it runs, and how often jobs overlap.
A GitHub Actions self-hosted Mac Runner receives work only when its labels and group permissions match the job requirements. GitHub documents runner labels and runner groups as mechanisms for organizing and controlling that access. Use them to route compatible jobs; do not assume that adding a node will fix a job that cannot match the pool.
Before sizing, separate Mac-required steps from portable work. A workflow can use different runner types for different jobs, provided dependencies and artifacts are passed between them correctly. That distinction can materially change Mac demand: the number of workflow runs is not the same as the number of Mac jobs.
Ask whether the queue is caused by insufficient eligible capacity or a routing mismatch. If a job waits while compatible nodes are idle, inspect labels, group access, and job-level runner selection before requesting another Mac. If compatible jobs repeatedly overlap and wait, record the duration and queue impact as evidence of demand.
Simulator tests versus ordinary builds: isolate only when the evidence requires it
Simulator work can consume a different mix of resources from a compile or archive job. Matrix tests and regression runs may create bursts of similar jobs, while simulator versions and test environment requirements can narrow the set of nodes that are suitable. Record those constraints by job type instead of folding all simulator minutes into a general build average.
The key decision is whether simulator tests can safely share nodes with ordinary builds. Shared capacity can use idle time efficiently, but it may also let a large test batch delay PR feedback. Separate pools can protect critical jobs, yet they create capacity that may sit unused when one workload is quiet. Compare queue records and job priorities before choosing either design.
For each simulator workflow, capture its required macOS and Xcode versions, destination settings, test data needs, and whether the test jobs can run concurrently without changing results. Validate any parallelization assumption in a pilot. Do not infer a safe concurrency level from elapsed build time alone: shared files, test accounts, simulator state, and artifact handling can all affect whether jobs remain independent.
A practical routing design uses labels to distinguish genuine environment requirements, not merely team ownership. If a test job needs a specific toolchain, give it a route that can satisfy that requirement. If the difference is only priority, consider whether workflow policy can protect urgent PR checks without maintaining a permanently separate pool.
Signing and release jobs: treat trust boundaries as capacity constraints
Archive, signing, and publishing tasks have a different risk profile from routine PR builds. Their execution may depend on protected environments, credentials, approval rules, or a limited release window. These controls can restrict which runner group may run a job even when other Macs appear idle.
Do not estimate signing capacity by dividing archive duration by the time available in a release window. First identify which jobs may use each credential and environment, who can trigger them, and whether policy allows concurrent signing tasks. A node that cannot access the required protected environment is not usable capacity for that job.
When a release workflow must be isolated, model it as its own pool or reserved capacity requirement. If it can share a node with ordinary builds under your security policy, document the routing and access controls that make this acceptable. Verify the actual behavior with a controlled run, including job permissions and cleanup after completion.
A long signing queue can signal a security boundary, not a shortage of Macs. Confirm that the waiting job is eligible for the available runners before changing capacity.
This distinction matters for both security review and budget approval. A separate node pool may be justified by policy even if the release jobs are infrequent. But if the restriction comes from a misconfigured runner group or label, buying or renting more hardware will not resolve it.
Release bursts and shared capacity: scale against observed waiting
Version releases, concentrated merges, and batch regressions can create a queue that looks alarming in a dashboard snapshot. Separate a short-lived burst from a persistent capacity gap by comparing queue duration and eligible concurrency across representative periods. A brief wait before a release may be acceptable; repeated delays to high-priority PR checks may not be.
Set an internal waiting target by job priority, then check whether the observed queue meets it. This is your organization’s service objective, not a GitHub runner metric. Also distinguish GitHub-hosted concurrency limits from the number of self-hosted Macs available to your organization. GitHub documents platform concurrency limits separately in its Actions limits reference; a hosted-runner limit should not be treated as a self-hosted Mac sizing recommendation.
For a variable workload, define a peak buffer from your own history and test it during a controlled pilot. Do not label an arbitrary reserve as a standard percentage or a GitHub requirement. If the queue clears quickly and does not breach your internal waiting target, a temporary burst may not justify permanent capacity. If high-priority jobs remain delayed during recurring peaks, test added eligible capacity and compare the resulting queue records.
GitHub describes Runner Scale Sets and their deployment and scaling configuration for managing runner capacity. These are implementation options, not evidence that a particular scale setting or node count fits your workload. Your model still needs to account for Mac availability, startup and maintenance behavior, routing, and the time your team can tolerate a queue.
Specialist toolchains and capacity model: turn records into a pilot decision
Some workflows require a particular macOS release, Xcode version, processor architecture, or other environment that the general pool cannot provide. Tag these jobs based on verified workflow requirements, then inspect whether the matching pool is actually busy when the jobs arrive. Do not merge incompatible work into a single capacity estimate just because both workflows run on macOS.
For each pool, calculate demand using consistent units. If a workload has a recorded job count and average runner time in the planning window, its total runner-hour demand is the job count multiplied by average runner time. Compare that demand with the usable runner-hours available in the same window. This gives a baseline utilization estimate, but it does not capture peaks, wait targets, or isolation restrictions.
Use a second measure for concurrency: the maximum number of simultaneously eligible jobs observed in the window, grouped by pool. Compare that figure with the waiting pattern and internal priority targets. Then account for nodes that are unavailable for maintenance or cannot serve a restricted workflow. The resulting estimate should show baseline capacity, peak capacity, and any deliberately reserved capacity as separate values. These are enterprise planning outputs, not official GitHub sizing figures.
A usable working model is:
- Baseline demand: recorded eligible job volume and execution time, divided by usable runner time for the same planning window.
- Peak requirement: observed overlapping eligible jobs, adjusted only by a burst assumption that you will test.
- Restricted capacity: the portion of demand that must use a specific toolchain, runner group, or signing boundary.
- Available capacity: online time after maintenance and other expected periods when a node cannot accept work.
- Expansion trigger: a sustained queue or missed internal wait target, rather than one isolated slow run.
Follow this rollout timeline before you approve a purchase, rental, or hybrid plan.
Evidence pass: Export workflow and runner records. Group jobs by PR checks, simulator tests, signing and release, and specialist toolchain. Verify that each group has a valid route.
Model pass: Calculate baseline demand and observed peak concurrency for each compatible pool. Write down the planning window, availability assumptions, and internal waiting targets so another reviewer can reproduce the estimate.
Pilot pass: Add or reassign a limited amount of capacity and observe queue delay, execution duration, and routing outcomes. Check that signing restrictions and simulator behavior remain correct. Use the pilot to revise assumptions rather than presenting a forecast as a measured result.
Decision pass: Expand only the pool that remains constrained after routing and policy checks. Choose owned capacity for stable, consistently utilized workloads when your team can manage the hardware lifecycle. Consider flexible Mac CI rental for temporary peaks or a trial, after checking that the listed configuration, rental term, and delivery method fit your toolchain and access needs.
Use this checklist before you approve the capacity plan:
- [ ] Separate macOS-required jobs from portable workflow steps.
- [ ] Record queue delay, execution time, eligible concurrency, and runner availability by workload.
- [ ] Confirm labels and runner group access for every job that appears in the capacity model.
- [ ] Identify simulator, signing, and specialist toolchain restrictions that require separate routing.
- [ ] Define internal waiting targets and peak assumptions from your own CI history.
- [ ] Validate the assumptions with a pilot and keep the resulting records with the budget request.
- [ ] Compare purchase, rental, and hybrid options using the same measured workload and availability assumptions.
A fixed Mac count without those records is difficult to defend: it hides which jobs need macOS, which are blocked by routing, and which only arrive in a release burst. Use the model to explain not only how much capacity you need, but when and why each pool is required.
A local or owned Mac fleet can provide predictable access for steady workloads, but it also leaves your team responsible for procurement timing, updates, failure recovery, and hardware that may be idle outside peak periods. A generic non-Mac runner cannot execute the macOS and Xcode jobs that require a real Mac environment. If your current setup has those costs or limits and the extra capacity is temporary, renting a Mac can be a better fit than buying for a short-lived peak. If you need stable, heavy use or physical access to peripherals, ownership may be the more suitable choice.
After you have workload records, compare the available Mac rental pricing and terms against your measured baseline and peak requirement. If the listed configuration and delivery details match your task requirements, you can review the Mac rental options and request a capacity trial before committing to a larger pool.