GitHub Actions can spin up a lot of redundant runs if you push code rapidly, especially for frequent checks like linting or tests. The concurrency key is your weapon against this, ensuring only the latest run for a specific job or workflow actually executes.
Let’s see it in action. Imagine a workflow that runs on pushes to main:
name: CI
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest
If you push five times in quick succession, you’ll get five separate CI workflow runs. If build is the only job, that’s five build jobs running in parallel, all checking the same code state (from the latest push).
Now, let’s introduce concurrency. We want to ensure that for any given branch, only one build job is running at a time.
name: CI
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }} # Unique group per workflow and ref
cancel-in-progress: true # Cancel previous runs in this group
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest
The concurrency block has two key fields: group and cancel-in-progress.
The group defines which runs are considered "concurrent" and thus eligible for cancellation. We’re using ${{ github.workflow }}-${{ github.ref }}. This creates a unique group for each workflow (CI in this case) on each specific branch (refs/heads/main). If you wanted to group all runs for a specific PR, you’d use ${{ github.workflow }}-pr-${{ github.event.number }}. For a specific tag, ${{ github.workflow }}-${{ github.ref }} works fine too.
cancel-in-progress: true is the crucial part. When a new run starts and its group is already occupied by a running job, this new run will cancel the previous job in that group. The new run then proceeds.
With this configuration, if you push those five commits quickly:
- The first push triggers workflow run #1. Job
buildstarts. - The second push triggers workflow run #2. The
buildjob in run #2 sees that run #1’sbuildjob is in the sameconcurrencygroup (CI-refs/heads/main) and is still running. Becausecancel-in-progressistrue, run #1’sbuildjob is cancelled. Run #2’sbuildjob then starts. - The third, fourth, and fifth pushes trigger runs #3, #4, and #5. Each new
buildjob cancels the previous one. - Ultimately, only the
buildjob from workflow run #5 will complete successfully. All earlier runs’buildjobs will be cancelled.
This strategy is invaluable for preventing resource waste and ensuring you’re always testing the absolute latest code. It’s particularly effective for jobs that are fast and run on every commit, like linters, formatters, and basic unit tests. For longer-running integration tests or deployments, you might choose a different concurrency strategy or even no concurrency at all, depending on your needs.
The concurrency key can be applied at the workflow level, affecting all jobs within that workflow, or at the job level, as shown above, allowing for finer-grained control. If applied at the workflow level, the group would typically be a workflow-wide identifier, like ${{ github.workflow }} or ${{ github.workflow }}-${{ github.event_name }}.
A common pitfall is not making the group identifier specific enough. If you use a generic group like my-build, and that group is used across multiple branches or different workflow files, you might accidentally cancel unrelated jobs. Always tie the group to the context that matters for your cancellation policy – usually the specific branch, PR, or tag.
When a run is cancelled due to concurrency, its status will be displayed as "cancelled" in the GitHub Actions UI, and you’ll often see a message indicating it was cancelled by a newer run. This is your explicit signal that the concurrency mechanism has done its job.
The next thing you’ll likely encounter is needing to manage concurrency across different environments or deployment stages, which often involves more complex grouping strategies or even manual approval gates.