A shorter GitHub Actions run is useful only if it returns trustworthy feedback. Removing checks, skipping affected components, or interrupting state-changing jobs can make a workflow appear faster while increasing release risk.
This playbook treats optimization as a controlled workflow change: identify the delay, change one bottleneck, verify both performance and protection, then retain a clear rollback path. The configurations below are illustrative rather than measured results; adapt commands, action versions, cache paths, matrix sizes, and runner labels to the repository.
Set the target before changing YAML
“CI is slow” can describe several different constraints. Select the metric that matches the actual complaint instead of optimizing total duration by default.
| Metric | What it reveals | Likely response |
|---|---|---|
| Queue time | Delay before a runner starts | Reduce obsolete runs or review runner capacity |
| Execution time | Time spent inside jobs | Cache dependencies, improve builds, or optimize tests |
| Time to first failure | How quickly a developer receives actionable feedback | Run fast, high-signal checks earlier or in parallel |
| Wall-clock duration | Elapsed time until all required jobs finish | Parallelize independent work or shard a dominant suite |
| Total runner usage | Combined execution across jobs | Remove repeated setup, excessive matrices, and irrelevant runs |
| Deployment lead time | Time from approval to verified deployment | Build once and deploy the already validated artifact |
Parallel jobs may improve wall-clock duration while increasing total runner usage. A larger runner can shorten a CPU-bound build without affecting queue time. Cancellation saves capacity but does not make an individual job execute faster.
Capture a baseline and a rollback boundary
Review multiple recent pull-request, main-branch, release, and scheduled runs. Record job durations, slow steps, queue delay, cache results, artifact transfer time, repeated setup, and where failures usually occur. A single run can be distorted by a cold cache, unusual change size, or temporary queue conditions.
Before rollout, define repository-specific acceptance conditions. For example:
- The required checks still run for every change that can affect their component.
- A clean cache miss produces the same valid result as a cache hit.
- All test shards collectively run the intended suite.
- Deployment jobs still consume the approved artifact and retain their existing protections.
- The selected timing metric improves across comparable runs without an unacceptable increase in runner usage or failures.
Keep the previous workflow revision available in version control. Most CI-only changes can be rolled back by reverting the workflow commit. Changes involving repository rules, required check names, runner infrastructure, or deployments also need those related settings restored.
Roll out optimizations from least disruptive to most structural
Make one category of change at a time. Combining cancellation, caching, path filters, sharding, and runner changes in one commit makes both diagnosis and rollback harder.
1. Cancel obsolete pull-request validation
When several commits are pushed to the same pull request, an older validation run may no longer be useful. A pull-request-specific concurrency group can cancel it:
name: Pull Request CI
on:
pull_request:
concurrency:
group: pr-${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
Verify this with a draft or test pull request by pushing a second commit while the first run is active. The older run should be cancelled and the newest commit should complete all required checks.
Do not apply this policy automatically to production deployments, database migrations, release publication, infrastructure changes, or other state-changing operations. Those workflows often need serialization rather than interruption. If deployment behavior is being redesigned as well, treat that as a separate change with an explicit recovery plan; see automating rollbacks for failed deployments.
2. Add dependency caching without making correctness depend on it
For Node.js, cache package-manager downloads and continue installing from the lock file:
- name: Check out repository
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
The lock file must participate in cache selection. A manual key may also need operating system, architecture, runtime version, package-manager version, or monorepo subproject boundaries.
Verify three cases: a first run with a miss, a repeated run with unchanged dependencies, and a run after changing the lock file. All must complete correctly. If stale or incompatible content appears, remove the caching configuration or narrow the key; the normal installation path should remain functional.
Never cache credentials, access tokens, signing keys, authenticated configuration, environment files containing secrets, or private package credentials.
3. Stop transferring artifacts that nobody consumes
A cache accelerates future work; an artifact stores or transfers a workflow output. Do not substitute one for the other.
Compiled packages needed by deployment are artifacts. Package-manager downloads are cache candidates. Screenshots, videos, and crash data may only be necessary after failure:
- name: Upload browser diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: end-to-end-diagnostics
path: |
test-results/
screenshots/
retention-days: 5
After changing uploads, force a representative failure and confirm that the expected diagnostics remain available. Also inspect the artifact paths for secrets or private data.
4. Parallelize meaningful units, not individual commands
Linting, type checking, unit tests, and some security checks can run independently. Parallel jobs reduce feedback time, but each job receives an isolated runner and may repeat checkout, runtime setup, and dependency installation.
Keep tasks together when setup costs more than the task, one command consumes another command’s output, or separation requires a large intermediate artifact. Split a large test suite only when the test framework supports deterministic sharding and all shards together preserve coverage.
Illustrative pull-request workflow
name: Pull Request CI
on:
pull_request:
permissions:
contents: read
concurrency:
group: pr-${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
quick-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm run lint
- run: npm run typecheck
unit-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 4
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- name: Run test shard
run: npm test -- --shard=${{ matrix.shard }}/4
- name: Upload failure diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-diagnostics-${{ matrix.shard }}
path: test-results/
retention-days: 5
build:
runs-on: ubuntu-latest
needs: [quick-checks, unit-tests]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: production-build
path: dist/
retention-days: 7
The sharding syntax is framework-specific. Confirm that every test is assigned exactly as intended, compare shard durations, and check for shared-state collisions. Equal file counts do not guarantee balanced execution times. If one shard dominates or parallel execution introduces instability, reduce the shard count or revert to the previous test job while the suite is reorganized.
5. Restrict triggers only after testing required-check behavior
Path filters can prevent an API workflow from running for unrelated changes:
on:
pull_request:
paths:
- "apps/api/**"
- "packages/shared/**"
- "package-lock.json"
- ".github/workflows/api-validation.yml"
The difficult part is identifying every shared input. Root lock files, schemas, generated code, shared packages, build scripts, and workflow files may all affect a component.
A workflow skipped by path or branch filtering can leave an associated required check pending and block merging. Test documentation-only changes, component changes, shared-file changes, and workflow changes against the actual repository rules. If merge behavior is incorrect, restore the broad trigger first, then redesign the required-check arrangement separately.
6. Reduce matrices without silently reducing support
A matrix multiplies jobs. Pull-request validation may use a smaller set of environments while broader compatibility runs on a schedule or before release, but only if that placement matches the project’s support and release policy.
| Question | Waste signal | Safer adjustment |
|---|---|---|
| Is the runtime supported? | Jobs cover versions no longer supported | Match the documented support policy |
| Must every combination run on each pull request? | Small changes run the complete grid | Keep essential PR coverage and run the broader grid at a defined release stage |
| Does the operating system alter behavior? | An environment unrelated to users or deployment is repeated | Retain systems the software actually supports |
| Is the Cartesian product meaningful? | The matrix creates unsupported combinations | Use explicit include and exclude entries |
7. Build once when deployment can reuse the output
Repeated compilation in validation, packaging, and deployment wastes work and can result in deployment of an output different from the one previously examined. Where the release design permits it, build an immutable output, validate it, upload it, and have deployment download that same artifact. Verify its digest before release.
This is not automatically faster: transferring a very large artifact can take longer than rebuilding. Treat a build-once change as a release-path modification, not a harmless cache adjustment. Validate artifact contents, retention, permissions, and deployment recovery before making it required.
8. Optimize Docker layers before changing runner size
Copy stable dependency definitions before frequently changing source files so that source edits do not unnecessarily invalidate dependency layers:
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
Review .dockerignore so local dependencies, Git history, temporary output, test media, environment files, and credentials do not enter the build context. BuildKit caching can help when layers remain reusable, but its value depends on Dockerfile order, architecture, branches, and build arguments.
Failure modes and immediate rollback actions
| Change | Failure signal | Immediate response |
|---|---|---|
| Concurrency cancellation | A release or migration is interrupted | Remove cancellation from state-changing workflows and restore serialized execution |
| Dependency cache | Misses fail or stale dependencies appear | Disable the cache or tighten its key; retain lock-file installation |
| Path filters | A required check remains pending or affected code is skipped | Restore the previous broad trigger and required-check configuration |
| Parallel tests | Shared ports, databases, or files collide | Reduce parallelism or return affected tests to a serial job |
| Test sharding | Tests are omitted, duplicated, or badly imbalanced | Revert to the complete suite and correct shard assignment |
| Artifact reduction | Failures no longer have enough diagnostics | Restore the required failure artifacts with narrowly defined paths |
| Build-once release | Deployment cannot retrieve or verify the artifact | Stop the rollout and restore the previously validated packaging path |
| Self-hosted runner | Jobs rely on residual state or expose sensitive access | Remove untrusted workloads and return them to an isolated approved runner |
Runner changes are capacity decisions, not YAML shortcuts
Larger GitHub-hosted runners are candidates for CPU-, memory-, disk-, or architecture-constrained work. They may provide little benefit when external downloads, serial test logic, or unnecessary triggers dominate.
Self-hosted runners add responsibility for patching, isolation, credentials, scaling, image maintenance, and incident response. Persistent runners may retain local state, so workflows must not depend on undeclared files from earlier jobs. Untrusted pull-request code must not gain access to sensitive infrastructure. These concerns align with broader zero-trust controls for DevOps environments; runner provisioning should also be reviewed like other managed infrastructure, including the practices discussed in this infrastructure-as-code tooling guide.
Post-change verification sequence
- Inspect workflow structure: confirm job dependencies and required check names in the visualization graph.
- Run a cache miss: verify the workflow succeeds without reusable cache data.
- Run a cache hit: confirm restoration occurs and the normal install command still runs.
- Exercise failure paths: confirm fast checks report promptly and diagnostic artifacts appear only where intended.
- Exercise trigger boundaries: test component, shared-file, documentation, and workflow-file changes.
- Push a superseding commit: confirm only obsolete validation is cancelled.
- Compare equivalent runs: review the target metric as well as total jobs, runner usage, and failure behavior.
- Retain rollback readiness: keep the previous workflow and related repository-rule settings identifiable until the change is accepted.
The useful endpoint is not the smallest number in the Actions interface. It is a workflow that returns relevant feedback sooner, avoids repeated work, and preserves the repository’s testing, security, and release guarantees.
Official references
- GitHub Actions workflow syntax
- Dependency caching reference
- Control workflow concurrency
- Matrix and job variations
- Store and share workflow artifacts
- Workflow visualization graph
- Larger GitHub-hosted runners
- Self-hosted runners
- Secure use reference
- Official actions/setup-node repository
- Docker Build Push Action documentation

Linqdev Technical Editorial Team maintains practical guides on cloud architecture, API security, DevOps, automation, and scalable systems. Articles prioritize primary documentation, standards, reproducible examples, trade-offs, and clearly labeled illustrative material. Corrections can be submitted through the Contact page.



