GitHub Actions Concurrency Explained | Prevent Overlapping Workflow Runs
In GitHub Actions, executions run in parallel by default—whether triggered by multiple rapid commits, concurrent pull requests, or distinct workflow files. Concurrency control allows DevOps engineers to define boundaries that prevent overlapping executions, avoid conflicting operations on shared resources, and save compute costs by canceling redundant pipeline runs.
1. Understanding Concurrency in GitHub Actions
Concurrency, in general software engineering, refers to tasks executing in parallel. In the context of GitHub Actions, concurrency describes scenarios where multiple workflow executions or job instances run simultaneously.
By default, GitHub Actions enforces no sequencing constraints between executions:
- Multiple Job Execution: Jobs defined within a single workflow file execute concurrently unless explicitly ordered via
needs:dependencies. - Multiple Workflow Executions: Triggering the same workflow multiple times in quick succession causes every trigger to spin up an independent, parallel execution run.
- Multi-Workflow Execution: Different workflow files located within the
.github/workflowsdirectory execute simultaneously when triggered by repository events.
Without concurrency limits, a new workflow execution starts immediately regardless of whether an earlier run of that same workflow is still actively processing.
2. Causes of Overlapping Workflow Executions
Overlapping workflow runs occur naturally during routine development workflows. Understanding how they occur helps identify where explicit concurrency boundaries are required.
Common trigger patterns that produce concurrent runs include:
- Rapid Successive Commits: A developer pushes Commit A, realizes a minor detail was missed, and immediately pushes Commit B and Commit C within seconds.
- Combined Event Triggers: A workflow configured to trigger on both
pushevents andworkflow_dispatch(manual/API triggers) can be invoked manually while an automated push execution is actively running. - Pull Request Updates: Re-rolling commits or force-pushing updates to an active Pull Request branch triggers fresh CI validation runs while previous runs are mid-execution.
3. Evaluating Concurrency: Beneficial vs. Problematic Scenarios
Allowing workflows to run concurrently is not inherently bad. Whether parallel execution is desirable depends on execution independence and resource isolation.
When Concurrent Runs Are Beneficial
Parallel executions accelerate feedback loops when workloads do not compete for state or infrastructure:
- Independent Branches & Pull Requests: Feature Branch A and Feature Branch B test separate code paths. Running their CI pipelines simultaneously provides rapid feedback to both developers without interference.
- Isolated Test Environments: If Workflow Run A targets
test-env-aand Workflow Run B targetstest-env-b, concurrent runs operate safely within their respective isolated resource boundaries. - Read-Only Validation Builds: Workflows that perform non-destructive tasks (such as static linting, unit testing, or vulnerability scanning) do not alter state and can safely run in parallel.
When Concurrent Runs Cause Production Failures
Parallel runs create severe issues when shared state, cloud infrastructure, or deployment targets are involved:
- Duplicate or Redundant Compute Costs: If Commit A, Commit B, and Commit C are pushed within a 1-minute window, running three complete image builds and container scans wastes billable GitHub Actions runner minutes.
- Conflicting Operations on Shared Infrastructure: If Workflow Run A updates an AWS Security Group's inbound rules while Workflow Run B simultaneously modifies the same Security Group, race conditions produce unpredictable network configurations.
- Infrastructure Dependency Collisions: If Workflow 1 builds a Virtual Private Cloud (VPC) and Workflow 2 deploys an application into that VPC, running both concurrently causes Workflow 2 to fail if the VPC creation is incomplete.
- Out-of-Order Deployment Race Conditions: If Commit B (containing latest updates) finishes its deployment job faster than Commit A (containing older code), Commit A's job completing second will overwrite production with outdated artifacts.
4. The Core Mechanism of Concurrency Groups
GitHub Actions manages parallel execution limits using Concurrency Groups. A concurrency group is a user-defined string key that establishes a scope of execution control.
Execution Rules within a Group
- Group Membership: Any workflow or job assigned to a specific concurrency group key is subject to that group's restrictions.
- Single Active Execution: GitHub Actions ensures that at most one execution belonging to the same concurrency group runs at any given time.
- Queueing & Replacement Logic: By default, a concurrency group allows at most one active run (
in_progress) and one pending run (pending).- If a third run is triggered while Run 1 is active and Run 2 is pending, GitHub Actions cancels the older pending run (Run 2) and places the newest run (Run 3) into the pending slot.
5. Managing Overlaps with cancel-in-progress
The cancel-in-progress boolean flag controls how GitHub Actions handles newly triggered runs when an existing run is already executing in the same concurrency group.
Strategy 1: Sequential Execution (cancel-in-progress: false)
- Behavior: If Run 1 is active, newly triggered runs wait in a queued/pending state until Run 1 completes.
- Use Case: Operations that must all complete sequentially without skipping steps (e.g., transactional database migrations or sequential auditing tasks).
Strategy 2: Immediate Cancellation (cancel-in-progress: true)
- Behavior: If Run 1 is active when Run 2 is triggered, Run 1 is immediately aborted and cancelled. Run 2 starts running without delay.
- Use Case: Developer feature branches where only the latest commit matters. Canceling intermediate builds saves compute time and prevents stale feedback.
Note: GitHub Actions supports queuing multiple runs (up to 100) using advanced queueing settings like
queue-max. However,queue-maxcannot be combined withcancel-in-progress: truebecause queuing pending runs conflicts logically with aborting active runs.
6. Workflow-Level vs. Job-Level Concurrency
Concurrency can be declared at two distinct structural levels within workflow YAML files depending on the required operational granularity.
Level 1: Workflow-Level Concurrency
Declaring concurrency at the root of a workflow YAML file controls all jobs inside that workflow execution as a single unit.
- Same Workflow Scope: Prevents multiple executions of the same workflow file from running simultaneously.
- Cross-Workflow Scope: If
workflow-a.ymlandworkflow-b.ymlwithin the same repository share an identical static concurrency group string, an active run ofworkflow-a.ymlwill blockworkflow-b.ymlfrom executing concurrently.
name: Global Infrastructure Workflow
on: [push]
concurrency:
group: infrastructure-group
cancel-in-progress: false
jobs:
provision:
runs-on: ubuntu-latest
steps:
- run: echo "Provisioning shared infrastructure..."
Level 2: Job-Level Concurrency
Declaring concurrency inside a specific job definition restricts only that targeted job while allowing preceding or succeeding jobs in the workflow to run concurrently.
- Targeted Control: Allows non-destructive early jobs (e.g.,
build,lint,test) to run in parallel across multiple runs, while isolating state-modifying jobs (e.g.,deploy-to-cluster). - Cross-Workflow Job Control: A job in
service-a.ymland a job inservice-b.ymlwill not run concurrently if both jobs specify the same concurrency group name.
name: Application CI/CD
on: [push]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- run: echo "Running parallel tests..."
deploy-to-production:
runs-on: ubuntu-latest
concurrency:
group: production-deployment-slot
cancel-in-progress: false
steps:
- run: echo "Deploying artifact to shared production target..."
7. Hands-On Walkthrough: Default Parallel Execution
To observe GitHub Actions' default unconstrained behavior, consider a workflow without concurrency configuration that simulates a long-running task using a 2-minute sleep step.
Base YAML Definition (No Concurrency)
name: GitHub Actions Concurrency Demo
on:
push:
branches:
- main
workflow_dispatch:
jobs:
concurrency-demo-job:
runs-on: ubuntu-latest
steps:
- name: Display Run Info
run: |
echo "Run ID: ${{ github.run_id }}"
echo "Commit SHA: ${{ github.sha }}"
- name: Simulate Long Running Task
run: |
echo "Simulating a long-running process (120 seconds)..."
sleep 120
- name: Complete Step
run: echo "Workflow completed successfully!"
Execution Behavior
- Triggering this workflow via a git push, followed immediately by triggering it twice manually via
workflow_dispatch, produces 3 simultaneous executions. - All three runs display an
in_progressstatus on the GitHub Actions dashboard at the exact same time.
8. Hands-On Walkthrough: Enforcing Concurrency (cancel-in-progress: false)
To block parallel execution and queue incoming runs sequentially, configure a static concurrency group with cancel-in-progress: false.
Workflow YAML with Group Concurrency
name: GitHub Actions Concurrency Demo
on:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: concurrency-demo
cancel-in-progress: false
jobs:
concurrency-demo-job:
runs-on: ubuntu-latest
steps:
- name: Display Run Info
run: |
echo "Run ID: ${{ github.run_id }}"
echo "Commit SHA: ${{ github.sha }}"
- name: Simulate Long Running Task
run: |
echo "Simulating a long-running process (120 seconds)..."
sleep 120
- name: Complete Step
run: echo "Workflow completed successfully!"
Observed Behavior During Rapid Triggers
- Trigger Run #5: Enters
in_progressimmediately. - Trigger Run #6: Enters
pending(waiting status with a clock icon). The UI states: "Waiting for GitHub actions concurrency demo #5 to complete before running." - Trigger Run #7 and #8:
- GitHub Actions allows only one pending run in a queue slot.
- Run #6 is automatically cancelled with the system message: "Cancelling this job... higher priority waiting request for concurrency demo exists."
- Run #8 replaces Run #6 in the single
pendingslot.
9. Hands-On Walkthrough: Active Cancellation (cancel-in-progress: true)
To implement an aggressive continuous integration strategy that immediately aborts outdated running builds in favor of new triggers, set cancel-in-progress: true.
Workflow YAML with Immediate Cancellation
name: GitHub Actions Concurrency Demo
on:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: concurrency-demo
cancel-in-progress: true
jobs:
concurrency-demo-job:
runs-on: ubuntu-latest
steps:
- name: Display Run Info
run: |
echo "Run ID: ${{ github.run_id }}"
echo "Commit SHA: ${{ github.sha }}"
- name: Simulate Long Running Task
run: |
echo "Simulating a long-running process (120 seconds)..."
sleep 120
- name: Complete Step
run: echo "Workflow completed successfully!"
Observed Execution Flow
- Trigger Run #11: Starts executing (
in_progress). - Trigger Run #12 (10 seconds later):
- GitHub Actions immediately aborts and cancels Run #11 mid-execution.
- Run #12 transitions straight into
in_progress.
- Trigger Run #13 (5 seconds later):
- Run #12 is immediately cancelled.
- Run #13 transitions to
in_progress.
10. Production Architecture: Dynamic Concurrency Groups
In real-world production repositories, hardcoding static group names like group: concurrency-demo introduces severe bottlenecks: a developer pushing to feature-a will inadvertently block or cancel CI builds occurring on feature-b or main.
To isolate concurrency boundaries per branch or per pull request, DevOps engineers construct Dynamic Concurrency Groups using context expressions.
Recommended Production Syntax
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Context Breakdown
${{ github.workflow }}: Evaluates to the workflow's name string, ensuring different workflow files do not block each other unless designed to do so.${{ github.ref }}: Evaluates to the Git ref triggering the run (e.g.,refs/heads/main,refs/heads/feature-login, orrefs/pull/42/merge).
Benefits of Dynamic Evaluation
- Branch Isolation: Commits pushed to
feature-loginevaluate to a unique group key, canceling previous runs onfeature-loginwithout affecting active runs onfeature-paymentsormain. - Resource Optimization: Unnecessary compute is aborted on active feature branches while preserving safety across shared branches.
11. Summary and Key Takeaways
- Default Behavior: GitHub Actions executes all workflow runs, jobs, and triggers in parallel by default without built-in queueing constraints.
- Concurrency Group Scope: A
concurrencygroup guarantees that at most one execution assigned to that group key runs at any single time across a repository. - Granular Placement: Concurrency can be declared at the workflow level (controlling all jobs in a run) or at the job level (isolating stateful operations like deployments).
- Sequential Queueing (
cancel-in-progress: false): Active runs finish normally; the single latest pending run waits in queue while intermediate pending runs are automatically cancelled. - Fast Abort (
cancel-in-progress: true): Active runs are immediately aborted upon the arrival of a newer trigger, giving priority to the latest code changes. - Preventing Production Pitfalls: Enforcing concurrency boundaries prevents billing waste, state corruption on shared infrastructure (e.g., Cloud Provisioning/VPCs), and out-of-order deployment race conditions.
- Production Best Practice: Always use dynamic group keys (e.g., {{ github.ref }}) to restrict concurrency limits per branch rather than blocking global repository execution.