The GitHub Actions runner, while trying to respect your concurrency limits, is misinterpreting the group identifier and cancelling valid, active workflows.
Here’s what’s actually going on and how to fix it:
- The Problem: GitHub Actions’
concurrencyfeature is designed to prevent too many workflows from running simultaneously for a given group. When it detects a conflict (a new workflow starting for a group that’s already at its limit), it cancels the newest workflow. The issue you’re seeing is that the group identifier isn’t being consistently interpreted, leading to legitimate workflows being cancelled when they shouldn’t be.
Let’s break down the common causes and their solutions:
1. Inconsistent concurrency Group Naming:
- Diagnosis: The most frequent culprit is a slight variation in how the
concurrencygroup name is defined across your workflows. Even a single space, capitalization difference, or missing character will make GitHub treat them as different groups.- Check: Manually inspect the
concurrencyblock in all workflows that might be using the same conceptual group. Look for subtle differences.
- Check: Manually inspect the
- Fix: Standardize the group name. Use a consistent, predictable naming convention. For example, if you want to limit concurrent runs for all CI jobs on your
mainbranch, use:
Or, if you want to limit by a specific branch, regardless of workflow name:concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true
Ensure this exact string (or pattern) is used everywhere.concurrency: group: ci-${{ github.ref_name }} cancel-in-progress: true - Why it works: GitHub uses the
groupstring as the key to track active workflows. If the keys don’t match precisely, it doesn’t recognize them as belonging to the same group, leading to incorrect cancellation.
2. Using github.run_id in the Concurrency Group:
-
Diagnosis: Including
github.run_idin yourconcurrencygroup definition is a common mistake.github.run_idis unique to each individual workflow run.- Check: Search your workflow files for
concurrency.group: ... ${{ github.run_id }}.
- Check: Search your workflow files for
-
Fix: Remove
github.run_idfrom the group definition. Theconcurrencygroup should represent a set of runs you want to limit, not a single run.# INCORRECT: # concurrency: # group: my-app-${{ github.run_id }} # CORRECT: concurrency: group: my-app-ci-${{ github.ref_name }} -
Why it works: By definition,
github.run_idis unique per run. If it’s part of the group name, every run will have a unique group name, effectively disabling the concurrency limit entirely and making it seem like everything is fine until you actually try to limit something else.
3. Overlapping Concurrency Groups:
- Diagnosis: You might have multiple
concurrencyblocks in a single workflow, or different workflows withconcurrencygroups that unintentionally overlap. This can create confusion for the GitHub Actions runner.- Check: Review all
concurrencyblocks in all workflows that could potentially run on the same repository or branch.
- Check: Review all
- Fix: Consolidate or clearly delineate your concurrency groups. If you have a global CI limit and a specific deployment limit, ensure their group names are distinct and don’t accidentally overlap. For example:
# Workflow A (CI) concurrency: group: ci-${{ github.ref_name }} cancel-in-progress: true # Workflow B (Deployment) concurrency: group: deploy-${{ github.ref_name }} cancel-in-progress: true - Why it works: Each
concurrencyblock defines a separate counting mechanism. If group names are too similar or if a single run could inadvertently belong to multiple groups due to complex logic, the runner might mismanage the counts.
4. Incorrectly Using cancel-in-progress:
- Diagnosis: The
cancel-in-progress: trueflag tells GitHub Actions to cancel the older workflow when a new one starts for the same group. If you intend to prevent the new one from starting and keep the old one running, this flag should befalseor omitted (asfalseis the default).- Check: Look for
cancel-in-progress: truein yourconcurrencyblocks.
- Check: Look for
- Fix: If you want the new run to be cancelled and the existing one to continue, ensure
cancel-in-progressis set totrue(which is the default behavior that cancels the new one). If you want the old run to be cancelled and the new one to proceed, setcancel-in-progress: false.
Or, if you want to cancel the OLD run and let the new one proceed:concurrency: group: my-app-${{ github.ref_name }} # This will cancel the NEW run if the group is full cancel-in-progress: trueconcurrency: group: my-app-${{ github.ref_name }} # This will cancel the OLD run and allow the new one to proceed cancel-in-progress: false - Why it works: This setting directly controls which job is terminated when a concurrency limit is hit. Misunderstanding this can lead to workflows being cancelled when you expected them to run, or vice versa.
5. Runner Specific Issues (Less Common):
- Diagnosis: In rare cases, especially with self-hosted runners, there might be network or agent-level issues causing communication delays or dropped heartbeats with the GitHub Actions control plane. This can make the runner appear "stale" and lead to premature cancellation.
- Check: Review runner logs for any network errors, timeouts, or disconnections. Check the runner’s system logs for resource exhaustion (CPU, memory, disk).
- Fix: Ensure your self-hosted runners have stable network connectivity to GitHub. Monitor their resource utilization. Update the runner agent software to the latest version:
# On the runner machine cd actions-runner ./bin/Runner.Service uninstall git pull ./bin/Runner.Service install - Why it works: A healthy, responsive runner agent is crucial for accurate state reporting to GitHub Actions, which is necessary for correct concurrency management.
6. Workflow Triggering Logic:
- Diagnosis: The way your workflows are triggered can sometimes lead to unexpected concurrency states. For instance, a
pushevent to a branch might trigger multiple workflows if it also matches aworkflow_dispatchorscheduleevent, especially if they share a concurrency group.- Check: Examine the
on:trigger for all relevant workflows.
- Check: Examine the
- Fix: Refine your triggers to be mutually exclusive where necessary. Use
if:conditions within youron:block or within jobs to prevent unintended simultaneous execution.on: push: branches: - main workflow_dispatch: inputs: deploy: description: 'Deploy to production?' required: true default: false type: boolean jobs: build: runs-on: ubuntu-latest steps: - run: echo "Building..." deploy: needs: build runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' && github.event.inputs.deploy == true concurrency: group: deploy-${{ github.ref_name }} cancel-in-progress: false steps: - run: echo "Deploying..." - Why it works: By being more precise about when each workflow should run, you reduce the chances of multiple workflows starting concurrently for the same conceptual task, thus avoiding concurrency conflicts in the first place.
After applying these fixes, the next error you’ll likely encounter is a workflow_run dependency issue if you’re using workflow_run triggers without properly accounting for their execution order and potential concurrency.