The problem nobody notices
GitHub will email you when a workflow fails. But when your CI takes twice as long as it should? You get silence. A slow build running on every pull request, every commit, compiling the same dependencies over and over—it's waste that hides in plain sight.
As of June 30, 2026, CI performance matters more than ever. Development teams are larger, deployments happen faster, and the cost of slow feedback loops compounds across the entire team. If your CI takes 15 minutes instead of 5, and your team opens 40 PRs a week, you are collectively burning 8 hours per week waiting for machines. That is a full person's worth of idle time. Yet most teams never see a dashboard alerting them to this problem.
Why it happens
GitHub Actions are simple to set up, which is part of why they spread everywhere. But that simplicity means sensible defaults often hide performance cliffs. You'll create a workflow, it works, and unless something breaks loudly, you stop thinking about it. Meanwhile, the workflow might be doing unnecessary work: reinstalling packages every run, uploading massive cache artifacts, running tests twice due to a typo in the workflow matrix, or using old images that take forever to boot.
The usual suspects are straightforward to find and fix. Let's walk through them.
Common culprits
Rebuilding dependencies every time
One of the easiest mistakes is not caching your dependencies. If your workflow installs the same npm packages, Go modules, or Python libraries on every run, you are paying the cost of the internet, unpacking, and validation multiple times per day for the exact same files.
Most languages have a built-in caching strategy. For Node.js, GitHub Actions provides a cache action that stores node_modules or your lock file between runs. For Python, you can cache pip wheels. Java has Maven and Gradle caches. If you are not using them, your CI is doing redundant work.
Slow or bloated images
GitHub Actions workflows run in containers. The image you choose sets a baseline for how long every run takes just to boot. Using an old or oversized base image (like a full Ubuntu with build tools you do not need) costs you minutes on every single run.
Switch to minimal images when you can. If you need specific tools, build a custom Docker image once, push it to a registry like GitHub Container Registry, and reuse it. The first run takes longer to build the image, but subsequent runs pull it instantly.
Running jobs twice by accident
Workflow matrices are powerful but easy to misconfigure. A common mistake is setting up a matrix for different Node versions or operating systems, then realizing you actually only meant to run it once. Each duplicate run burns CPU, storage, and time.
Review your workflow YAML. If you see a matrix that is duplicating work unnecessarily, flatten it or add a condition to run only on certain branches.
Large or unnecessary artifacts
If your workflow uploads large build artifacts or logs after every run, GitHub will spend time storing and compressing them. If you are not actually using those artifacts downstream, they are pure waste.
Be explicit about what you upload. Do you need the entire build directory, or just the final binary? Do you need logs from successful runs, or only on failure? Set a retention policy so old artifacts do not pile up.
Serial steps that could run in parallel
If your workflow runs lint, then tests, then build, all in sequence, you are using only one CPU core for part of the time. GitHub Actions can run multiple jobs in parallel by default. If you have independent steps, split them into separate jobs.
A simple example: linting is fast and does not need the full build. Run it as its own job. If it fails, you know immediately without waiting for tests. If it passes, tests and build run at the same time.
How to measure slowness
Before you optimize, you need a baseline. GitHub provides a workflow timeline in the Actions tab. Open a recent run, and look at each job and step's duration. You will almost always spot at least one step that is much slower than it should be.
Write down the numbers. After you make changes, come back and compare. Seeing "CI went from 12 minutes to 5 minutes" is satisfying and proves the fix worked.
Step-by-step optimization checklist
Step 1: Enable dependency caching
Add a caching step for your language. For Node.js, add this to your workflow:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
The cache: 'npm' line tells GitHub to save and restore your node_modules between runs automatically. Other languages have similar options—check the official action for yours.
Step 2: Audit your base image
If your workflow uses runs-on: ubuntu-latest, check what that image includes. If you only need Node.js, consider switching to the official Node image (docker://node:20) or GitHub's lightweight version. If you need a custom setup, build and push a minimal image once.
Step 3: Split slow jobs into parallel jobs
Review your workflow YAML. If you have sequential jobs that don't depend on each other, split them. For example:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test
Now lint and test run at the same time instead of lint-then-test.
Step 4: Remove redundant artifacts
Check your workflow for actions/upload-artifact. If the artifact is not used by another job or downloaded manually, remove it. If you keep artifacts for debugging, set a short retention time:
- uses: actions/upload-artifact@v4
if: failure()
with:
name: logs-on-failure
retention-days: 7
This uploads logs only on failure and deletes them after a week.
Step 5: Check for matrix duplication
Search your workflow for matrix: and review each one. If the matrix is not actually changing the behavior you want, remove it. A matrix that runs every build twice is dead weight.
Step 6: Review step order
Within a job, each step runs sequentially. Make sure the order makes sense. If a step is slow but its result is only used much later, consider moving it closer to where it is used. If a step is fast and independent, move it to the front so it fails early.
Monitoring over time
Once you have optimized, do not forget about it. CI workflows drift—dependencies get larger, new steps get added, images change. Set a monthly reminder to spot-check your slowest workflow. If it is trending slower, investigate before it becomes a crisis.
Some teams export GitHub Actions metrics to a monitoring dashboard. This is advanced, but if you have many workflows, it pays for itself by surfacing drift automatically.
Conclusion
Slow CI is a tax on your team's velocity that never appears on a bill. It compounds quietly across hundreds of runs per week. The fixes are not hard—caching, parallel jobs, removing waste—but they require seeing the problem first. Spend an hour this week reviewing your workflows. Odds are you will find 5-10 minutes of wasted time per run. That is a gift to your future self.
Merits
- Faster feedback loops keep developers unblocked and in flow.
- Reduced cloud compute costs as runs complete quicker.
- Early detection of problems (e.g., split jobs fail earlier).
- Better developer experience; people are happier waiting 3 minutes than 15.
- Small changes compound across dozens of runs per week into significant time savings.
- Easy to measure; dashboard data is already available in GitHub.
Demerits
- Requires audit and maintenance; workflows do not stay optimized without review.
- Over-aggressive caching can mask dependency bugs until production.
- Splitting jobs too much creates complexity; more jobs mean more things to debug.
- Minimal images sometimes lack tools you need later, requiring rework.
- Artifact cleanup policies risk losing logs you need for debugging.
- Parallelization helps only if you have enough concurrency quota; GitHub's free tier is limited.
Caution
All names, configuration values, and placeholders in this article (e.g., node:20, ubuntu-latest, app.example.com) are for example purposes only. Before deploying any workflow to production, test it thoroughly on a staging branch. Your specific setup, language versions, and infrastructure may differ. Caching strategies that work well for one project may not suit another. Always verify performance improvements in your actual environment, and monitor for side effects (e.g., stale cache causing outdated dependencies). Proceed at your own risk.
Frequently asked questions
- What is the maximum cache size per repository on GitHub Actions?
- How do I know if GitHub Actions caching is actually being used in my workflow?
- Can I use a custom Docker image to speed up my CI?
- Why does my first run after creating a cache take longer, not shorter?
- How do I parallelize jobs in GitHub Actions without increasing concurrency quota usage?
- Is it better to cache node_modules or just the lock file?
- Can slow CI runs cause GitHub to cancel my workflow automatically?
- How often should I review and update my GitHub Actions workflows?
Tags
#github #actions #ci #cicd #devops #performance #optimization #workflows #automation #testing


Responses
Sign in to leave a response.
Loading…