GitHub Actions Concurrency Limits, Explained

GitHub Actions concurrency limits come from three places: the workflow concurrency key, your plan's concurrent job ceiling (lower for macOS), and runner pool capacity. How to get past each one.

Last verified:

GitHub Actions has three concurrency limits. The workflow concurrency key holds or cancels runs in a group, so scope each group to one workflow and branch. Your plan's concurrent job ceiling caps GitHub-hosted jobs, with a lower macOS ceiling, so cut demand, upgrade, or move jobs to WarpBuild runners. Runner pool capacity limits self-hosted labels, so add capacity.

What are the three GitHub Actions concurrency limits?

The three limits are the concurrency key you write in workflow YAML, the concurrent job ceiling of your GitHub plan, and the capacity of the runner pool behind your runs-on labels. Each one queues work in a different way, so identify the limit before you change any YAML.

1. The workflow-level concurrency key

The concurrency key is a limit you write yourself. At the top of a workflow it controls whole runs. Inside a job it controls that job across runs. Every run or job with the same group name shares one lane.

A concurrency group allows at most one running job or workflow run. Others in the group are pending. With the default queue: single, at most one item can be pending, and GitHub cancels the older pending item when a new one arrives. With queue: max, up to 100 items can be pending, and GitHub cancels new items when the queue is full. With cancel-in-progress: true, a new run also cancels the item that is running.

The key never makes anything start sooner. It only holds or cancels work, so it produces pending and canceled runs, not faster ones.

2. Plan-level concurrent job ceilings

Standard GitHub-hosted runners have a fixed ceiling on concurrent jobs. The ceiling depends on your GitHub plan, and macOS has its own lower ceiling.

PlanConcurrent jobsConcurrent macOS jobs
Free205
Pro405
Team605
Enterprise50050

Jobs above the ceiling wait until a slot is free. The ceiling applies to the whole account, so a large matrix in one repository can delay the workflows of every other repository. Larger GitHub-hosted runners have separate limits that GitHub documents on the same page.

3. Runner pool capacity

Jobs that target self-hosted or managed labels are outside the plan ceilings. Their queue depends on how many matching runners can come online. For a static self-hosted fleet, that is the number of registered machines. For an autoscaled fleet, it is the controller maximum, such as the max runners setting on an ARC scale set. For a managed provider, it is the capacity the provider has behind the label.

A job whose labels match zero online runners never starts. On self-hosted runners, GitHub cancels a job after 24 hours in the queue. That looks like a capacity problem but is a routing problem. The common issues doc lists the registration and permission checks that tell the two apart.

Which concurrency limit is queueing your jobs?

The symptom tells you which limit it is. Pending or canceled runs inside one concurrency group mean the concurrency key. A running job count that stops at your plan number means the plan ceiling. One label that queues while others start means pool capacity for that label.

SymptomLimit
A run is pending while another run in the same concurrency group runsWorkflow concurrency key
A pending run is canceled when a newer run enters the same groupWorkflow concurrency key
Running job count stops at 20, 40, 60, or 500 while the rest queuePlan ceiling
macOS jobs queue while Linux jobs on the same plan startPlan ceiling, macOS ceiling
Jobs on one self-hosted label queue while jobs on other labels startPool capacity for that label
Jobs on a label never start (self-hosted jobs are canceled after 24 hours)Wrong labels or runner group access

Measure queue time before you change anything. For jobs on GitHub-hosted or self-hosted runners, subtract each job's created_at from its started_at in the workflow jobs REST API. A queue that rises during working hours on GitHub-hosted runners points to the plan ceiling. A queue that stays high on one self-hosted label all day points to pool capacity for that label.

For jobs on WarpBuild runners, the Queue p75 / p90 column on the Jobs page of WarpBuild reports shows the wait per job.

If queue wait is high and none of the three limits fits, the guide to GitHub Actions queue times covers the other causes.

How do you get past each concurrency limit?

Scope the concurrency group to one workflow and ref, get past the plan ceiling by cutting demand, upgrading, or changing runs-on, and fix pool capacity by adding runners. The concurrency key costs nothing to fix, so start there if it matches.

Fix the concurrency key

Most concurrency key problems come from a group with the wrong scope. Scope each group to one workflow on one ref:

  1. Build the group name from ${{ github.workflow }}-${{ github.ref }} so two branches never share a lane.
  2. Set cancel-in-progress with an expression such as ${{ github.ref != 'refs/heads/main' }}. Pull request pushes then cancel stale runs, and main always finishes.
  3. Give deploy jobs their own job-level group per environment without cancellation. Deploys then wait for each other instead of canceling each other.
  4. Search for a static group name shared across workflows. A bare concurrency: production in three workflows forces all three into one lane.

Get past the plan ceiling

The ceiling on standard GitHub-hosted runners has no setting you can change. You have three options:

  1. Reduce peak demand. Use path filters so documentation changes skip the test matrix.
  2. Upgrade the plan. A higher plan moves the concurrent job ceiling from 20 to 40, 60, or 500.
  3. Change runs-on. The ceiling counts jobs on GitHub-hosted runners only, so a job on another runner pool is not counted.

WarpBuild runners are one such pool. Generally available features on its Linux and Windows runners support unlimited concurrency, so workflows can run any number of jobs and any number of workflows in parallel. Features in beta may have limits. For macOS, see the next section.

Fix pool capacity

For a self-hosted fleet, add machines or raise the autoscaler maximum, and plan for the idle cost of that extra capacity. For a managed pool, choose one with enough capacity for your matrix.

Teams that want the runners inside their own cloud account can use BYOC (bring your own cloud), which runs WarpBuild runners on AWS, GCP, and Azure. The number of jobs you can run at once then depends on the quotas in your cloud account. For example, the GCP BYOC docs list the CPU, disk, and IP address quotas needed for each concurrent job.

What can you do when your macOS CI queue is always backed up?

A macOS queue that stays long while Linux jobs start is usually the macOS plan ceiling. GitHub allows 5 concurrent macOS jobs on Free, Pro, and Team plans and 50 on Enterprise. This macOS maximum is shared across standard and larger GitHub-hosted runners, so moving to a larger GitHub-hosted Mac runner does not add slots.

You have these options:

  1. Run only Apple-specific steps on macOS. Linting, unit tests for shared code, and other jobs that do not need Xcode can run on Linux and do not use macOS slots.
  2. Skip macOS jobs when they are not needed. Use path filters so a change that does not touch the iOS or macOS app does not start a macOS build.
  3. Upgrade to GitHub Enterprise for a higher macOS ceiling.
  4. Move macOS jobs to WarpBuild macOS runners. They run on Apple Silicon M4 Pro and M5 Pro. These jobs do not count against the GitHub plan ceiling. If you need more concurrent macOS jobs than your current limit, contact [email protected] to raise it.

The change is one line per job:

jobs:
  ios-tests:
    runs-on: warp-macos-latest-arm64-6x
    steps:
      - uses: actions/checkout@v7
      - run: ./scripts/ios-test.sh

Labels, specs, and per-minute rates for every size are on the macOS Runners for GitHub Actions: Sizes and Pricing page.

How do you configure a matrix that fans out without hitting a limit?

Combine a concurrency block that limits runs per branch with a matrix on a runner label that the plan ceiling does not count. The workflow below fans one test suite out to twelve parallel jobs on one WarpBuild Linux label.

name: test-suite
on:
  pull_request:
  push:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
  test:
    runs-on: warp-ubuntu-latest-x64-2x
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
      - run: npm ci
      - run: npm test -- --shard=${{ matrix.shard }}/12

What each part does:

  • The group expression gives every branch its own lane, so a push to a feature branch never holds a run on main.
  • The cancel-in-progress expression cancels old runs on pull request branches and lets main runs finish.
  • fail-fast: false lets the other eleven shards finish when one fails, so a flaky shard does not hide real results.
  • The matrix creates 12 jobs per run. GitHub limits a matrix to 256 jobs per workflow run.
  • If a downstream service rate-limits you, add max-parallel under strategy to run fewer jobs at once than the matrix size.

The warp-ubuntu-latest-x64-2x label runs on this WarpBuild cloud runner:

runs-on labelOSvCPURAMRate per minute
warp-ubuntu-latest-x64-2xUbuntu 24.0428GB$0.004

Linux runners come in sizes from 2 to 32 vCPU, and Windows runners from 4 to 32 vCPU. The full Windows list, including Windows Server 2025 and the Visual Studio 2026 labels, is on the Windows Runners for GitHub Actions: Sizes and Pricing page.

For deploys, move the group to the job level and remove cancellation. Releases then wait for each other instead of stopping a rollout in progress:

  deploy:
    needs: test
    runs-on: warp-ubuntu-latest-x64-2x
    concurrency:
      group: deploy-production
    steps:
      - run: ./scripts/deploy.sh

A typo in a label leaves a job queued with no logs. If a job never starts, work through the common issues checklist before you treat it as a capacity problem.

How much time does removing a concurrency limit save, and what does it cost?

Removing a concurrency limit shortens wall clock time but does not change billed minutes. The example below uses the 12-shard matrix above.

Example assumptions: each of the 12 shards takes exactly 8 minutes, shards are uniform with no retries, and billing is per minute. Total compute is 12 x 8 = 96 job-minutes per run in every scenario.

Behind a ceiling. The suite runs on standard GitHub-hosted Linux 2-core (x64) runners on the Free plan, and other workflows leave 4 of the 20 slots free. The 12 shards run through 4 slots in 3 waves of 8 minutes each.

Fanned out. The same suite runs on warp-ubuntu-latest-x64-2x, which the plan ceiling does not count. All 12 shards start together.

Scenario (example)Available slotsWavesWall clockBilled minutes
Behind a shared ceiling4324 min96
Fanned out on a label with no ceiling1218 min96

In this example, fanning out saves 16 minutes of wall clock per run. At 30 runs a day, that is 480 minutes of waiting removed each day, with no extra billed minutes.

The table below prices the 96 minutes on matched 2 vCPU runners at the GitHub-hosted rate and the WarpBuild rate.

Runner (2 vCPU)Rate per minuteBilled minutesCost per run
GitHub-hosted Linux 2-core (x64)$0.00696$0.58
warp-ubuntu-latest-x64-2x (8GB)$0.00496$0.38

The per-minute rate sets the bill. Full rates by runner type are on the pricing page. If single shards are also slow, Why Are My GitHub Actions Builds Slow? covers the runner-side causes.

FAQ

Does the concurrency key limit how many jobs run in parallel?

No. The concurrency key allows one running job or workflow run per named group. By default one more can be pending, and GitHub's workflow syntax reference documents a queue: max option that allows up to 100 pending. The number of parallel jobs inside a run comes from your plan's job ceiling and from runner pool capacity.

Do GitHub's concurrency limits apply to self-hosted runners?

GitHub's per-plan concurrent job ceilings apply to GitHub-hosted runners. Jobs that target self-hosted or managed labels wait on pool capacity instead. Workflow limits such as the 256-job matrix cap still apply.

How many jobs can a single matrix create?

GitHub limits a matrix to 256 jobs per workflow run. Beyond that, split the suite into several workflows or remove a matrix dimension.

Does WarpBuild limit concurrency?

Generally available features on WarpBuild Linux and Windows runners support unlimited concurrency, so workflows can run any number of jobs and any number of workflows in parallel. Features in beta may have limits.

How do I get more concurrent macOS jobs?

On GitHub-hosted runners, GitHub allows 5 concurrent macOS jobs on Free, Pro and Team plans and 50 on Enterprise. You can move macOS jobs to WarpBuild macOS runners, which run on Apple Silicon M4 Pro and M5 Pro. If you need more concurrent macOS jobs there, contact [email protected] to raise your limit.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.