Stop wasting CI minutes: use concurrency to cancel redundant GitHub Actions runs

Continuous integration is most useful when it gives fast, relevant feedback. But when contributors push a flurry of commits or update a pull request repeatedly, GitHub Actions can queue many runs that test old commits — wasting minutes, slowing feedback, and increasing costs. The concurrency feature in GitHub Actions gives you a simple way to ensure only the most recent run for a branch or PR proceeds while older, redundant runs are canceled automatically. This quick guide explains what concurrency does, how cancellation behaves, and how to add it safely to a CI workflow.

Why concurrency matters for CI

How concurrency works (the essentials)

A minimal example: cancel previous CI runs on a branch Add concurrency near the top-level of the workflow file (right after on:). This example cancels any in-progress or pending workflow runs for the same branch, so only the latest push to main is executed.

name: CI

on:
  push:
    branches: [ main ]

concurrency:
  group: ci-$
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "run tests"

This groups runs by the branch reference (github.ref) and cancels older runs for that branch when newer ones start. The behavior and syntax are documented in the GitHub Actions concurrency docs. (docs.github.com)

Canceling per-pull-request (useful for PR checks) Pull requests use different contexts. A common pattern is to group by github.head_ref so that updates to the same PR cancel previous checks. Because github.head_ref is only defined for pull_request events, include a fallback to avoid syntax errors when the workflow may run for other events:

concurrency:
  group: $
  cancel-in-progress: true

This keeps PR runs unique per branch name (head ref). If head_ref is undefined, run_id ensures uniqueness and avoids an expression error. (docs.github.com)

Job-level concurrency You can apply concurrency at the job level if you want to limit concurrency for a particular job rather than the whole workflow. This is handy if only one job (for example, deployment) must be serialized while other tests can run in parallel.

jobs:
  deploy:
    runs-on: ubuntu-latest
    concurrency:
      group: deploy-$
      cancel-in-progress: true
    steps:
      - run: echo Deploying...

Be aware: job-level concurrency groups are evaluated independently of workflow-level groups. Use distinct group names if you don’t want different concurrency scopes to interact. (docs.github.com)

What cancellation actually does (and what it doesn’t) When a workflow or job is canceled, GitHub follows a defined cancellation process:

Implications and gotchas

Useful patterns

Practical tips to pair with concurrency

Short checklist before enabling concurrency on a repo

Summary Adding concurrency to your GitHub Actions CI is a low-friction, high-impact change: a small YAML block prevents redundant runs, saves minutes, and speeds feedback for developers. Use dynamic concurrency group names to scope cancellation to branches or PRs, include cancel-in-progress when you want the newest commit to win, and watch for the cancellation behavior details so long-running cleanup steps behave correctly. The official GitHub Actions docs describe the syntax and cancellation process in detail. (docs.github.com)

References