Concurrency Control & Path Filters
Cancel superseded runs and skip irrelevant ones so CI is fast, cheap, and never races itself.
Two settings quietly save enormous amounts of CI time and money: concurrency stops redundant runs from piling up, and path filters stop workflows firing when nothing they care about changed.
Concurrency
A concurrency group ensures only one run per group proceeds at a time. Set cancel-in-progress: true and a fresh push to a PR instantly cancels the now-stale run for that branch β no more three builds racing for the same commit.
- Key the group by branch/PR:
group: ci-${{ github.ref }}. - For deploys, do the opposite: keep
cancel-in-progress: falseso an in-flight production deploy is never killed mid-way.
Path filters
Under on.push / on.pull_request, paths and paths-ignore limit a workflow to relevant changes β don't run the full test suite for a README typo. For required status checks, prefer paths at the job level or a path filter action, since a skipped required check can block merges.
Example
name: CI
on:
pull_request:
paths:
- "src/**"
- "package.json"
paths-ignore:
- "**/*.md"
# One live run per branch; a new push cancels the stale one
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm testWhen to use it
- A team adds concurrency groups so only one deployment workflow runs at a time per branch, cancelling queued runs when a newer push arrives.
- A monorepo uses path filters so a push that changes only docs/ does not trigger the expensive build-and-test workflow.
- A developer uses cancel-in-progress: true so a second push to a PR branch immediately cancels the still-running CI from the first push.
More examples
Cancel in-progress runs per branch
Groups concurrent runs by workflow name and ref; when a new push arrives on the same branch, the in-progress run is cancelled immediately.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
pull_request:Path filters to skip unchanged services
The workflow only triggers when files under services/api/ or the shared package change, skipping runs for unrelated changes elsewhere in the monorepo.
on:
push:
paths:
- 'services/api/**'
- 'packages/shared/**'
pull_request:
paths:
- 'services/api/**'
- 'packages/shared/**'
jobs:
test-api:
runs-on: ubuntu-latest
steps:
- run: npm test --workspace=services/apiProtect deployments with concurrency
Setting cancel-in-progress: false queues deployments instead of cancelling them, ensuring every merge to main is eventually deployed in order.
jobs:
deploy-prod:
runs-on: ubuntu-latest
concurrency:
group: production-deploy
cancel-in-progress: false # queue, don't cancel
environment: production
steps:
- run: ./scripts/deploy.sh
Discussion