DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding `concurrency: cancel-in-progress` to the CI workflow to cut wasted GitHub Actions minutes

Adding concurrency: cancel-in-progress to the CI workflow to cut wasted GitHub Actions minutes

TL;DR: I added the concurrency block with cancel-in-progress: true to the ci-e2e.yml workflow. The change stops overlapping runs on the same branch, instantly freeing minutes that were previously consumed by stale jobs.


The Problem

Our repository craveview runs an end‑to‑end test suite on every push and on a scheduled cron. The workflow lives in .github/workflows/ci-e2e.yml. Because developers push frequently, especially during feature‑branch development, we often saw two or more CI runs queued for the same branch:

Run actions/checkout@v3
Run setup-node@v3
Run npm ci
Run npm run test:e2e
Enter fullscreen mode Exit fullscreen mode

When a second push arrived before the first run finished, GitHub started a new job while the previous one kept running. The older run completed its 12‑minute test suite even though its results were obsolete. This inflated our monthly GitHub Actions minutes bill and slowed feedback for the team.

The symptom was simple: the Actions tab showed multiple runs for the same branch, and the Usage page reported a steady increase in minutes even when no new code was merged.


What I Tried First

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

steps:
  - name: Check for running jobs
    run: |
      if gh run list --workflow ci-e2e.yml --branch ${{ github.ref_name }} --status in_progress; then
        echo "Another run is in progress, exiting."
        exit 0
      fi
Enter fullscreen mode Exit fullscreen mode

I used the GitHub CLI (gh) to list in‑progress runs and bail out early. The idea worked locally, but it introduced a race condition: two pushes could still start the guard step at the same time, both see no running jobs, and both continue. Moreover, the extra CLI call added ~5 seconds to every run and required a GITHUB_TOKEN with additional permissions, which felt like over‑engineering for a problem that GitHub already solves.


The Implementation

GitHub Actions provides a built‑in concurrency feature that groups runs by a key and optionally cancels any in‑progress run when a new one arrives. The syntax is straightforward and does not require extra steps or tokens.

Diff Overview

The commit 76536eca modified only one file, adding a 7‑line block:

@@ -9,6 +9,13 @@ on:
  # de Actions; el objetivo (detectar drift externo) no requiere diario
  - cron: "40 6 * * 1,4"

+# Cancel previous runs on the same branch if a new push arrives
+# This prevents duplicate work and saves CI minutes.
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
Enter fullscreen mode Exit fullscreen mode

Full ci-e2e.yml after the change

Below is the complete workflow with the new concurrency block highlighted:

name: CI – E2E

on:
  push:
    branches:
      - main
      - develop
      - feature/*
  pull_request:
    branches:
      - main
      - develop
  schedule:
    # Run nightly on Monday and Thursday at 06:40 UTC
    - cron: "40 6 * * 1,4"

# --------------------------------------------------------------
# NEW: Cancel previous runs on the same branch
# --------------------------------------------------------------
concurrency:
  # The key combines the workflow name and the ref (branch or tag)
  group: ${{ github.workflow }}-${{ github.ref }}
  # When a new run is triggered, abort the old one
  cancel-in-progress: true

jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.x]
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: Install dependencies
        run: npm ci

      - name: Run E2E tests
        run: npm run test:e2e
Enter fullscreen mode Exit fullscreen mode

Why this group key?

  • ${{ github.workflow }} resolves to CI – E2E.
  • ${{ github.ref }} resolves to the full ref, e.g. refs/heads/feature/login.
  • The concatenation gives a unique key per workflow‑branch pair, ensuring that only runs on the same branch cancel each other. Pull‑request runs (which use a ref like refs/pull/123/merge) get their own group, so they don’t interfere with branch pushes.

What happens under the hood?

When a new push triggers the workflow:

  1. GitHub checks if there is an existing run with the same group.
  2. If found and cancel-in-progress: true, the older run receives a cancel event.
  3. The runner stops the job, marks it as Cancelled, and frees the allocated minutes.
  4. The new run proceeds normally.

No additional code, no external CLI, no extra permissions. The feature is declarative and works across all runners (self‑hosted or GitHub‑hosted).

Verifying the change

After merging the PR, I opened a new branch, pushed two commits in rapid succession, and watched the Actions tab:

  • Run #1 started, then quickly changed status to Cancelled as Run #2 began.
  • The Usage page showed ~12 minutes saved (the average runtime of the cancelled job).

I also added a quick sanity check in the workflow logs:

- name: Show concurrency key
  run: echo "Concurrency group: ${{ github.workflow }}-${{ github.ref }}"
Enter fullscreen mode Exit fullscreen mode

The log printed the expected key, confirming that the interpolation works as intended.


Key Takeaway

Use GitHub Actions’ built‑in concurrency with cancel-in-progress instead of custom scripts to prevent overlapping CI runs. It’s a one‑liner, requires no extra permissions, and instantly reduces wasted minutes—especially valuable for high‑frequency push environments.


What's Next

Now that duplicate runs are eliminated, the next step is to parallelize the E2E suite to bring the total runtime down further. I plan to:

  1. Split the test suite into shards using npm-run-all and a matrix strategy.
  2. Add a cache for node_modules and Cypress binary to shave off install time.
  3. Monitor the new workflow’s cost impact and adjust the concurrency group if we introduce additional workflow files (e.g., ci-unit.yml).

Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México

Tags: #vibecoding #buildinpublic #github-actions #ci #devops #javascript #cypress


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

Repo: zaerohell/craveview · 2026-10-01

#playadev #buildinpublic

Top comments (0)