GitHub·7 min read·

Working with Artifacts and Dependency Caches in GitHub Actions

Share build outputs between jobs with GitHub Actions artifacts and speed up repeated installs with dependency caches — storage limits and patterns included.

NB

Netbay Engineering

Netbay Engineering

On this page

Two features separate a workable pipeline from a fast one: artifacts, which carry build outputs between jobs, and caching, which stops you re-downloading the same dependencies on every run. Both are easy to misuse, and both have hard limits that surprise people. This post explains what each does, the storage constraints, and the patterns that make CI faster without breaking correctness.

Artifacts: sharing files between jobs

Artifacts persist files produced in one job so later jobs can download them. They are the right tool for passing a built bundle, a test report, or a deploy package from a build job to a test or deploy job. Because each job runs on a fresh runner, artifacts are how data travels between them. Upload with the upload-artifact action, download with download-artifact.

The key mental model: artifacts are for output, not for reusable inputs that a build could recreate. They carry a snapshot of files across job boundaries. Use them when the producing job's working directory would not exist in the consumer.

yaml
jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: bundle
          path: dist/
  deploy:
    runs-on: ubuntu-22.04
    needs: build
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: bundle
          path: dist/
      - run: ls dist/

Storage limits and retention

Artifacts consume storage and have retention windows. The exact free limits change, so check GitHub's current documentation, but the rules of thumb are: keep artifacts small, set an explicit retention-days to avoid clutter, and never use artifacts as a database. Retention defaults to a fixed number of days; you can set retention-days per upload to purge temporary build outputs sooner, which keeps the repo's artifact tab tidy and storage costs down.

Use path filters and include-exclude with find to upload only what matters, not the whole working tree. And remember each artifact download in a later job is network cost — aggregate several files into one artifact when a single consumer needs them.

Caches: speeding up repeated installs

Caches store dependency packages or build intermediates keyed to a hash of the inputs that produced them. The canonical pattern is to cache a package manager's install directory keyed by the lockfile hash. If the lockfile changes, the cache misses and dependencies re-install; if unchanged, the cache hits and installs are nearly instant.

The cache action is also object storage with limits — total cache size per repository and per-package individual limits apply. Exceeding them simply means the cache is evicted or not restored; correctness never depends on a cache hit, but performance does.

yaml
jobs:
  test:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            npm-${{ runner.os }}-
      - run: npm ci
      - run: npm test

Cache keys and restore-keys

The key determines when a cache is restored. If key matches exactly, restore is direct. If it misses, the restore-keys list gives fallback prefixes, so the most recent cache with a matching prefix is restored even when the lockfile changed. This trades a little stale-ness for a big speed win — restore the old node_modules, then let npm ci fix only what changed, or re-prune.

Cache hits are not guaranteed and a cache can be silently evicted. Treat caching strictly as an optimization and never as a correctness mechanism: your install step must be idempotent whether or not the cache returned a hit.

Artifacts vs caches: pick the right one

It is worth a clear comparison, because people conflate them. Artifacts move build outputs between jobs and carry an immutable snapshot; caches store reusable dependency/build intermediates for the winner across runs, and are mutable/evictable. Use artifacts when the producer's output must be consumed downstream in the same run. Use caches when the goal is reproducing work faster on future runs. Uploading a dependency folder as an artifact instead of caching it is the most common mistake — it leaks storage and is deleted after retention, so it helps no later run.

artifacts outputs across jobs, immutable caches inputs across runs, mutable+evictable build -> deploy node_modules / oci layers keyed on lockfile hash restore-key fallback keeps you fast

Takeaway

Artifacts carry output forward within a run; caches make future runs faster. Keep artifacts small with tight retention, key caches on the files that determine their contents, and treat caching strictly as an optimization. When your final job has a built artifact ready to deploy, ship it to an Ubuntu VPS on Netbay, which you can provision in under 60 seconds — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance