GitHub Actions allows only one running and one pending job or workflow in a concurrency group; a newly queued pending run can replace the existing pending run, according to the official concurrency documentation. That documented limit is a useful warning: queued runs do not automatically equal a shortage of Mac machines. There is no fixed Mac count that fits every team. First measure overlapping macOS work, queue time, and runner occupancy during representative busy periods. Estimate capacity against an explicit waiting-time goal, then add a small amount of capacity and verify the result before expanding further.
Small iOS teams can use this approach to decide whether current waits justify another Mac runner.
Multi-repository teams can use it to divide shared capacity by project and workload.
Platform leads can use it to plan self-managed runners from records they can audit.
Estimate GitHub Actions iOS parallel-build Mac capacity from observed work
Start with a distinction that often gets lost in capacity discussions: a workflow run is not necessarily a single runner slot. A run can contain jobs with dependencies, jobs that can execute in parallel, and jobs that target different runner types. The resource to estimate is the number of eligible macOS jobs that can execute at once, not the number of workflow runs or developers.
For each relevant job, capture three things:
- Queue wait: the time from when the job becomes eligible to when it starts.
- Runner occupancy: how long the job holds a runner while executing.
- Overlap: how many eligible macOS jobs need a runner during the same period.
The workflow jobs API can help teams inspect job-level records. Combine that evidence with workflow logs and runner-side monitoring. Do not assume every interval between workflow creation and completion is runner occupancy: a job can wait on dependencies, approval, or other workflow logic before it is ready to execute.
A simple workload model can make the estimate reviewable. For a chosen observation window, let W be the total runner-occupancy time of eligible macOS jobs that must finish within that window, and let T be the amount of time available to serve that workload. The ratio W / T describes average slot demand for that window. It does not guarantee an acceptable queue, because work may arrive in bursts and jobs may not be interchangeable.
Use the ratio as a starting point, then compare it with observed peak overlap and the queue target for each workload. If the team needs release jobs to start promptly but can tolerate longer waits for routine tests, estimate those classes separately. The result should be a capacity range with its assumptions written down, not a platform-wide promise or a universal Mac count.
For solo developers, separate a burst from a persistent bottleneck
A solo developer can see a queue after pushing several changes close together, particularly when a workflow runs tests and a full build. One busy afternoon is weak evidence for permanent expansion. The key question is whether work regularly arrives faster than the existing runner can complete it, or whether a temporary burst simply created a short backlog.
Collect data over a representative development cycle that includes ordinary commits and the team’s known busier periods. Track queue wait, runner occupancy, job outcome, and whether a later workflow superseded or canceled an earlier one. The concurrency setting matters here: as the official documentation explains, a concurrency group can keep one pending run while replacing its pending predecessor. That behavior may be intentional for workflows where only the latest commit matters, but it can make a queue look different from one where every run must complete.
Use these checks before adding capacity:
- Did several independent macOS jobs need a runner at the same time, or was one job waiting for an earlier job in its own workflow?
- Were eligible runners actually busy, or were they idle because a label or access rule excluded them?
- Does the queue recur across representative periods, or did it coincide with an unusual burst of pushes?
- Does every intermediate build need to finish, or can the workflow safely prioritize the latest commit?
- Did a build hold the runner for its whole occupancy interval, or did setup, dependency installation, and cleanup account for much of that time?
If the runner is busy whenever an eligible job waits, additional capacity is worth testing. If the runner is idle, adding another Mac may leave the same queue in place. Inspect job dependencies, labels, and concurrency behavior first.
A slow workflow does not prove a runner shortage. A queue forms when eligible work cannot start; a long-running job can be slow without creating a queue if no other work is waiting.
For small teams, plan around overlapping work and a wait target
For a small team, developer headcount is not a reliable proxy for iOS Runner count. People do not all push at the same time, and a single person can trigger several jobs. Conversely, a team can have concurrent changes but little macOS work if workflows filter paths, cancel obsolete builds, or hold jobs behind dependencies.
Begin by classifying workflow jobs according to their purpose. Unit tests, full application builds, and release jobs may have different occupancy patterns and different urgency. Keep the classification practical: the goal is to identify work that competes for the same macOS execution slots, not to create a reporting taxonomy that nobody maintains.
Then set a wait target for each class. A team might accept a queue for routine verification while treating a release job as time-sensitive. Write down what counts as a meaningful wait for that team before changing capacity. Without a target, “the queue feels too long” tends to turn into a permanent request for more machines even when the cause is workflow design.
Estimate demand using overlapping eligible jobs during the target periods. Review both the average workload and the busier intervals. If a release period repeatedly creates a backlog that routine work cannot clear before its deadline, test whether a separate release route or temporary burst capacity is more appropriate than raising permanent capacity for every workflow.
GitHub’s workflow syntax documentation describes workflow configuration, including controls that affect which jobs run and how they are organized. Use those controls intentionally. For example, a job dependency that prevents work from starting is different from a job waiting because all eligible runners are occupied. The first calls for workflow analysis; the second may justify more execution slots.
For multi-repository teams, choose shared routing or isolation deliberately
When several iOS repositories use macOS CI, adding machines is only one part of the decision. The team must also decide which repositories can use which runners, how jobs are routed, and whether the shared pool can meet each project’s access and release requirements.
A shared runner group can make sense when projects have compatible tooling, ownership, and access needs. It can pool variable demand: one repository may be quiet while another has a burst of tests. GitHub documents runner groups as a way to organize runners and control which repositories or workflows can use them. Review the current runner-group documentation before designing access and routing.
Project-specific routing can be more suitable when teams need clear ownership, different permissions, or separation between release and routine work. Isolation may make capacity less flexible: an idle runner reserved for one project cannot necessarily serve another. The trade-off is not simply shared versus dedicated machines. It is pooled utilization versus the operational boundaries the team needs.
Split the workload before comparing those options:
- Unit-test jobs: assess how frequently they arrive and whether they can use a common route.
- Full builds: measure their runner occupancy separately, especially when they compete with tests.
- Release jobs: document access controls, priority, and whether they need a route that routine jobs cannot consume.
Do not infer a GitHub-hosted or self-hosted concurrency allowance from a planning formula. The capacity model estimates what a team might need; it does not establish platform quotas or billing. Check the current official rules for the organization’s plan and runner arrangement. For self-managed runners, the self-hosted runner documentation explains the model, while the runner reference covers configuration and runner selection.
Diagnose whether the queue needs more Macs or a workflow change
Queue time is important, but it does not identify the cause by itself. Runner monitoring and troubleshooting records can help determine whether jobs were waiting because runners were busy, unavailable, or ineligible. Use the official monitoring and troubleshooting guidance alongside job-level records.
Look for these patterns:
- Busy eligible runners plus waiting jobs: capacity is a plausible constraint, particularly if the pattern repeats during the periods that matter.
- Idle runners plus waiting jobs: investigate labels, runner-group access, job targeting, and workflow dependencies.
- Long occupancy with little overlap: optimize the slow job before increasing the pool. More runners may not shorten one job’s elapsed time.
- Backlogs after release bursts: compare a temporary capacity increase or a separate release route with permanent expansion.
- Failures that rise after expansion: inspect runner health, resource contention, configuration differences, and job routing; more slots do not guarantee a more reliable build.
Keep API or log data aligned with the question being asked. A job-level view helps explain queue and execution behavior; aggregate workflow duration alone can hide whether time was spent waiting, executing, or blocked by dependencies. Preserve the same definitions when comparing before and after a change.
Use a staged estimate and check it off before expanding
The following checklist turns an estimate into an experiment. Each item should have an owner or a recorded result, so the team can revisit the decision rather than relying on memory.
- [ ] Select a representative observation window that includes ordinary work and the team’s known busy periods.
- [ ] Record eligible macOS job arrivals, queue waits, runner occupancy, cancellations, and failures.
- [ ] Separate jobs that require every run to finish from jobs where a newer run can replace an older pending run.
- [ ] Group work into tests, full builds, and releases where their urgency or occupancy differs.
- [ ] Set an explicit acceptable queue target for each class before changing capacity.
- [ ] Compare actual runner availability with waiting jobs; check labels, group access, and dependencies when runners are idle.
- [ ] Estimate average workload and busy-period overlap separately. Keep the assumptions beside the result.
- [ ] Make a limited capacity or routing change, then compare queue wait, utilization, and failure patterns against a comparable period.
- [ ] Keep the change only if the measured improvement addresses the original wait target without introducing unacceptable reliability or access issues.
For a more direct queue-based estimate, define a deadline for a batch of work. Add the runner-occupancy time of the jobs that need to finish by that deadline, then compare that total with the service time available across eligible slots. This gives a workload-based lower bound under the assumptions that the jobs can run independently and that their target runners are available. It is not a guarantee: dependencies, uneven arrival times, job duration variance, and routing restrictions can still create waits.
Avoid inventing a universal utilization target. Instead, use the team’s observed queue behavior and service requirements to decide whether the current pool has enough headroom. A capacity estimate should state its observation window, workload classes, excluded anomalies, and queue objective. If those inputs change, revisit the estimate.
A cache hit can change runner occupancy without changing the number of workflows. Record cache conditions and unusual jobs so a before-and-after comparison does not credit extra Macs for a separate build change.
Frequently asked questions
How can queue time help estimate Mac runner capacity?
Measure when jobs become eligible and when they start, then compare that wait with runner occupancy and other work waiting at the same time. Queue time alone cannot reveal the required capacity. Long waits can also come from labels, concurrency rules, or workflow dependencies. Use a representative busy period and confirm the likely cause before adding runners.
How should a team allocate macOS CI concurrency across several iOS repositories?
Separate workloads by operational needs first. Tests, full builds, and release jobs may have different priorities, permissions, and scheduling constraints. A shared runner group can serve multiple repositories when routing and access controls fit. Isolate projects when release risk, required tooling, or ownership makes shared access unsuitable. Review routing and queue data before choosing either model.
Should runner capacity match average work or peak concurrency?
Neither average demand nor the highest observed overlap should dictate permanent capacity by itself. Average demand can hide short release-time queues, while matching a brief peak can leave machines idle later. Measure representative periods, define an acceptable wait for each job class, and test a limited capacity change. Keep burst capacity temporary when peaks are short.
How can a team tell whether a queue needs more runners or workflow changes?
Compare queued time with runner occupancy and inspect whether eligible runners were available when jobs waited. If runners were busy, added capacity may help. If eligible runners were idle, investigate labels, group access, concurrency controls, dependencies, or serial steps. After a change, compare queue duration, utilization, and failures over a comparable workload period.
Map the measured requirement to a Mac plan
A self-managed Mac pool is not automatically the best answer for every CI queue. It can leave a team paying for idle capacity between bursts, while a shared pool can create routing or ownership constraints. A local Mac avoids remote provisioning, but it ties build availability to a specific machine and its maintenance. These trade-offs matter more than choosing a machine count from team size.
First complete the estimate using the team’s own Actions records. If the evidence shows that temporary or remote Mac capacity fits better than buying and maintaining another device, review Zutcloud’s published Mac mini rental options and confirm the current configuration and delivery terms that apply. For questions about access or setup, the Zutcloud help center provides a place to verify service details before mapping the estimate to a rental plan. If the workload is continuous and stable, or requires physical interfaces unavailable in a remote setup, owning a dedicated Mac may remain the better operational choice.
The Mac capacity decision should follow the queue evidence, not precede it. A small trial can show whether added runner slots shorten the wait; if the queue persists while eligible machines are idle, fix routing or workflow dependencies before renting or deploying more Macs.
FAQ
How can queue time help estimate the number of Mac runners?
Measure when jobs enter the queue and when they start, then compare that wait with each job’s runner occupancy and the other jobs waiting at the same time. Queue time alone cannot tell you the required capacity: long waits may come from runner labels, concurrency rules, or workflow dependencies. Use a representative busy period and confirm the likely cause before adding runners.
How should a team share macOS CI capacity across several iOS repositories?
Separate workloads by their operational needs first: tests, full builds, and releases may have different priorities, permissions, and scheduling constraints. A shared runner group can serve multiple repositories when routing and access controls fit. Keep projects isolated when release risk, required tooling, or ownership makes shared access unsuitable. Review routing and queue data before choosing either model.
Should Mac runner capacity match average demand or peak concurrency?
Neither figure should dictate permanent capacity by itself. Average demand can hide short release-time queues, while matching the highest observed overlap may leave machines idle for much of the cycle. Measure workload across representative periods, define an acceptable wait for each job class, and test a small capacity change. Keep burst capacity temporary if peaks are brief.
How do you tell whether CI queues need more runners or workflow changes?
Compare queued time with runner occupancy and inspect whether eligible runners were available when jobs waited. If runners were busy with useful work, added capacity may help. If eligible runners were idle, investigate labels, runner-group access, concurrency controls, dependencies, or serial steps. After each change, compare queue duration, utilization, and failures over a comparable workload period.
Further reading
- Compare team iOS CI/CD options, runner concurrency, and signing isolation
- Learn how parallel build stages and cache strategy affect cloud Mac capacity
Add Dedicated Mac Capacity to Your CI Pipeline
Deploy a dedicated Apple Silicon Mac mini with Zutcloud to give your iOS runners predictable, isolated compute.
Choose a 16GB or 24GB unified memory configuration to match your build concurrency and workload needs. Order now