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’ concurrency feature 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 concurrency group 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 concurrency block in all workflows that might be using the same conceptual group. Look for subtle differences.
  • 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 main branch, use:
    concurrency:
    
      group: ${{ github.workflow }}-${{ github.ref }}
    
      cancel-in-progress: true
    
    Or, if you want to limit by a specific branch, regardless of workflow name:
    concurrency:
    
      group: ci-${{ github.ref_name }}
    
      cancel-in-progress: true
    
    Ensure this exact string (or pattern) is used everywhere.
  • Why it works: GitHub uses the group string 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_id in your concurrency group definition is a common mistake. github.run_id is unique to each individual workflow run.

    • Check: Search your workflow files for concurrency.group: ... ${{ github.run_id }}.
  • Fix: Remove github.run_id from the group definition. The concurrency group 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_id is 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 concurrency blocks in a single workflow, or different workflows with concurrency groups that unintentionally overlap. This can create confusion for the GitHub Actions runner.
    • Check: Review all concurrency blocks in all workflows that could potentially run on the same repository or branch.
  • 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 concurrency block 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: true flag 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 be false or omitted (as false is the default).
    • Check: Look for cancel-in-progress: true in your concurrency blocks.
  • Fix: If you want the new run to be cancelled and the existing one to continue, ensure cancel-in-progress is set to true (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, set cancel-in-progress: false.
    concurrency:
    
      group: my-app-${{ github.ref_name }}
    
      # This will cancel the NEW run if the group is full
      cancel-in-progress: true
    
    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 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 push event to a branch might trigger multiple workflows if it also matches a workflow_dispatch or schedule event, especially if they share a concurrency group.
    • Check: Examine the on: trigger for all relevant workflows.
  • Fix: Refine your triggers to be mutually exclusive where necessary. Use if: conditions within your on: 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.

Want structured learning?

Take the full Github-actions course →