on
Make CI cheap and fast for small teams: smart caching + selective runs
Small engineering teams usually have two constraints: limited time and limited CI budget. That makes CI speed and predictability more important than polished orchestration. Two simple levers produce the biggest impact with low maintenance: 1) cache the expensive bits (dependencies, build outputs) so runs are fast, and 2) avoid running full pipelines when changes don’t need them (docs, comments, minor formatting). Below I explain why these work, show minimal examples, and share practical trade-offs to watch for.
Why this matters now
- Caching in GitHub Actions is a first-class feature and has evolved recently (newer action versions and separate restore/save primitives) — using the right version and strategy reduces repeated compute and download time. (github.com)
- Empirical studies show caching is widely adopted but requires ongoing maintenance: cache keys, restore heuristics and version updates change frequently, so the win comes from good patterns, not one-off tweaks. (arxiv.org)
- Skipping unnecessary runs (using path filters or simple skip rules) is an inexpensive way to cut CI waste when teams frequently push non-code changes. Community and Ops guides recommend these filters to save minutes and cost. (docs.github.com)
Pattern 1 — Cache the slow, repeatable work
What to cache:
- Language dependencies (node_modules, pip cache, .m2).
- Built artifacts that are expensive to produce but reused across jobs.
- Tooling downloads (CLI binaries, compiler caches).
Minimal GitHub Actions pattern
- Restore cache early.
- Skip install/build steps if cache hit is exact.
- Save cache at the end using a stable key.
Example (npm + lockfile):
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Restore dependencies cache
id: deps-cache
uses: actions/cache/restore@v5
with:
path: node_modules
key: $-node-$
- name: Install dependencies
if: steps.deps-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Run tests
run: npm test
- name: Save dependencies cache
if: always()
uses: actions/cache/save@v5
with:
path: node_modules
key: $
Notes:
- Use a hash of lockfiles (hashFiles) so the cache updates only when dependencies change. The actions/cache action provides explicit restore/save primitives and a cache-hit output you can use to skip steps. (github.com)
- Pin to a supported cache action version and ensure self-hosted runners meet the minimum runner version when relevant.
Trade-offs and tips
- Don’t cache volatile build outputs unless they’re stable across runs; invalid cache keys create subtle failures.
- Track cache-hit rates (add a step to log cache-hit) for a month — if hits are low, adjust key strategy.
- Expect maintenance: lockfile formats, runtime/tool versions, and action versions change over time — plan for occasional updates. Empirical work shows caching configs evolve often. (arxiv.org)
Pattern 2 — Run less by running only what changed
Simple ways to avoid waste:
- Use paths and paths-ignore to avoid running a workflow for docs-only or config-only changes.
- Use commit message directives (where your team agrees on them) or a light “skip CI” action if you need flexible opt-out.
- Cancel duplicate runs for the same branch so only the latest commit builds.
Example: skip CI when only docs changed
on:
push:
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
Caveats
- Be careful with required checks and branch protection: path-ignore might prevent required statuses from running and block merges if used incorrectly. Test the behavior in a repo you control. Community guides and GitHub documentation cover filter semantics and gotchas (both the filter behavior and interactions with required checks). (docs.github.com)
- For fine-grained skipping (e.g., skip only a particular job), you can evaluate the changed files in a small first job and conditionally run later jobs.
Keep it maintainable
- Centralize common patterns as reusable workflows or composite actions to avoid copy-paste. That reduces divergence, but teams report reuse has friction (versioning, trust, and debugging) so start small and pin references. (docs.github.com)
- Use observable metrics: record run duration, cost per run, and cache-hit ratio in a simple dashboard or CSV. Even a weekly CSV export helps justify changes.
- Automate routine updates: dependabot-style tools can help bump action versions and lockfile formats so your caching strategy stays compatible.
Final perspective
For small teams, the biggest ROI in CI comes from a couple of modest, repeatable practices: cache the heavy, repeatable parts of your builds and stop running everything for trivial changes. These moves are low-friction, easy to test, and—when paired with a lightweight maintenance routine—deliver consistent speed and cost savings without adding a DevOps headcount. (github.com)