How to Speed Up GitHub Actions
A GitHub Actions pipeline that reinstalls every dependency from scratch and runs all test suites serially can balloon to 40+ minutes. Two changes fix most of that: cache the dependencies with actions/cache and parallelize the suites with a strategy.matrix.
Why GitHub Actions pipelines go slow
Two patterns cause most of the pain on a growing Python (or Node) project:
- No dependency caching - every job does a fresh
pip install -r requirements.txt, downloading and building the same packages on every run. - Serial test suites - unit, integration, and e2e tests queue up inside a single job, each waiting for the one before it to finish.
Both are easy to fix with standard GitHub Actions primitives.
Cache pip dependencies with actions/cache
actions/cache@v4 saves and restores a directory between runs. For pip, cache
~/.cache/pip keyed on the hash of requirements.txt - so the cache is
invalidated automatically whenever dependencies change:
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ hashFiles('requirements.txt') }}
On a cache hit, pip install -r requirements.txt becomes a near-instant no-op
(packages are already present). On a cache miss the install runs normally and
the result is saved for the next run.
Parallelize test suites with strategy.matrix
strategy.matrix spins up one runner per value in the matrix, all at the same
time. Replace a single bloated job with a matrixed job:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
suite: [unit, integration, e2e]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ hashFiles('requirements.txt') }}
- run: pip install -r requirements.txt
- run: pytest tests/${{ matrix.suite }}/
With three suites and three runners, wall-clock time drops from
unit + integration + e2e minutes to max(unit, integration, e2e) minutes.
Combined with caching, a 40-minute pipeline commonly reaches 3-5 minutes.
Further improvements
- Restore keys - add a
restore-keys: pip-fallback so a partial cache hit still saves most of the install time whenrequirements.txtchanges. - Per-suite caches - cache
pytestresult caches (~/.cache/pytest) or Nodenode_modulesunder their own keys for even faster incremental runs. - Larger runners - GitHub's bigger runners (8-core) speed up heavy compilation or test suites that are CPU-bound rather than I/O-bound.
- Test-impact analysis - tools like
pytest-testmononly run tests that touch changed files, skipping suites entirely on unrelated PRs.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Caching pip dependencies with actions/cache@v4 keyed on requirements.txt
- Parallelizing test suites across runners with strategy.matrix
- Using matrix context variables (matrix.suite) to run the right test command per runner
FAQ
How do I cache pip in GitHub Actions?
Use actions/cache@v4 with path set to ~/.cache/pip and key set to pip-${{ hashFiles('requirements.txt') }}. On a cache hit, pip install becomes a near-instant no-op because all packages are already present.
How do I run GitHub Actions jobs in parallel?
Use strategy.matrix on the job and list the values to parallelize over (e.g. suite: [unit, integration, e2e]). GitHub spins up one runner per value simultaneously, so wall-clock time drops to the duration of the slowest suite rather than the sum of all suites.
Why is my GitHub Actions pipeline so slow?
The two most common causes are downloading dependencies from scratch on every run (fix: actions/cache) and running all test suites in sequence inside a single job (fix: strategy.matrix to parallelize them across runners).
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →