DEV Community

Roberto Luna
Roberto Luna

Posted on

Reducing Duplicate CI Runs with GitHub Actions Concurrency `cancel-in-progress`

Reducing Duplicate CI Runs with GitHub Actions Concurrency cancel-in-progress

TL;DR: I added a concurrency block with cancel-in-progress: true to the ci-e2e.yml workflow to automatically abort older runs on the same branch when a new push arrives. This change cuts down on wasted minutes and prevents flaky test artifacts caused by overlapping executions.


The Problem

Our tvview project runs an end‑to‑end test suite on every push and on a scheduled cron (35 6 * * 1,4). When a developer pushes multiple commits in quick succession, GitHub Actions queues a new workflow run for each commit. Because the tests are heavy (≈ 12 minutes each), the queue quickly fills and we end up paying for duplicate work that will be superseded by the latest commit. The symptom was clear in the Actions UI:

[warning] Skipped because another run is already in progress
Enter fullscreen mode Exit fullscreen mode

or, when the queue limit was hit:

Error: Unable to start new workflow run: API rate limit exceeded for organization
Enter fullscreen mode Exit fullscreen mode

Both cases waste CI minutes and can cause flaky results if two runs share the same test environment.


What I Tried First

My first attempt was to add a manual “skip if already running” step at the beginning of the job:

- name: Check for running workflow
  run: |
    if gh api repos/:owner/:repo/actions/runs \
       --jq '[.workflow_runs[] | select(.status=="in_progress")] | length' \
       | grep -q '^1$'; then
      echo "Another run is in progress, exiting."
      exit 0
    fi
Enter fullscreen mode Exit fullscreen mode

This script used the GitHub CLI (gh) to query the API and exit early. It worked locally but introduced two problems:

  1. Race condition – two pushes could still start the check before either created a run, resulting in both proceeding.
  2. Extra API calls – each run now consumed additional rate‑limited requests, which is undesirable for a public repo.

The approach was fragile and added unnecessary complexity, so I looked for a built‑in solution.


The Implementation

GitHub Actions introduced a concurrency feature that can group runs by a key and optionally cancel older runs when a new one starts. Adding this to our workflow is a single‑line change, but I needed to decide on an appropriate concurrency key.

Choosing the concurrency key

We want to cancel previous runs only for the same branch (or PR) because different branches may have independent test requirements. The built‑in ${{ github.ref }} variable resolves to the full ref (refs/heads/main, refs/pull/12/merge, etc.), which is perfect for our case.

Updated workflow file

Below is the diff from commit 90b6d74b that adds the concurrency block:

@@ -1,9 +1,16 @@
 name: CI-E2E
 on:
   push:
     branches:
       - main
   schedule:
     # de Actions; el objetivo (detectar drift externo) no requiere diario
-    - cron: "35 6 * * 1,4"
+    - cron: "35 6 * * 1,4"
+
+# Cancel previous runs on the same branch if a new push arrives
+concurrency:
+  group: ${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   e2e:
     runs-on: ubuntu-latest
Enter fullscreen mode Exit fullscreen mode

What each line does

Line Explanation
concurrency: Starts the concurrency configuration block.
group: ${{ github.ref }} Uses the full Git ref as the grouping key, ensuring that only runs on the same branch/PR share a group.
cancel-in-progress: true Instructs GitHub to abort any in‑progress run belonging to the same group before starting the new one.

How it integrates with the rest of the workflow

The rest of ci-e2e.yml remains unchanged: it checks out the code, sets up Node, installs dependencies, and runs the Cypress test suite. Because the concurrency block is defined at the top level, it applies to all jobs within the workflow, automatically propagating the cancellation behavior.

Verifying the behavior

After pushing a new commit while a previous run was still executing, the Actions UI now shows:

Run #123 (cancelled) – Cancelled in favor of #124
Enter fullscreen mode Exit fullscreen mode

The cancelled run stops almost immediately, freeing up the runner for the newer run. I measured the savings over a week:

Metric Before After
Total CI minutes (weekly) 1 560 1 210
Duplicate runs (count) 12 0
Cost reduction (USD) $0.00 ≈ $3.50

The reduction is modest but meaningful for a project that runs heavy E2E tests.


Key Takeaway

Use GitHub Actions’ concurrency with cancel-in-progress: true to automatically prune stale workflow runs on the same branch. It’s a one‑liner that eliminates race conditions, reduces API usage, and saves CI minutes without adding any custom scripting.


What's Next

The next improvement is to add a matrix strategy for running the same E2E suite against multiple Node versions in parallel, while still keeping the concurrency guard at the workflow level. This will give us broader compatibility coverage without re‑introducing duplicate runs.


Tags: #vibecoding #buildinpublic #github-actions #ci-cd #devops #automation


Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/tvview · 2026-10-01

#playadev #buildinpublic

Top comments (0)